Skip to main content

django-crispy-crud

PyPI version License: MIT

Reusable Django CRUD views, forms, and template fragments with HTMX, Alpine.js, and Tailwind CSS.

Installation

With uv:

uv add django-crispy-crud

With pip:

pip install django-crispy-crud

For audit log support (optional):

uv add "django-crispy-crud[audit]"
# or
pip install "django-crispy-crud[audit]"

Setup

Add crispy_crud to your INSTALLED_APPS:

INSTALLED_APPS = [
    # ...
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "crispy_forms",
    "crispy_tailwind",
    "django_htmx",
    "django_filters",
    "crispy_crud",
]

Add the required middleware:

MIDDLEWARE = [
    # ...
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django_htmx.middleware.HtmxMiddleware",
]

Ensure the messages context processor is included in your template settings:

TEMPLATES = [
    {
        # ...
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.request",
                "django.contrib.messages.context_processors.messages",
                # ...
            ],
        },
    },
]

Set crispy forms to use Tailwind:

CRISPY_ALLOWED_TEMPLATE_PACKS = "tailwind"
CRISPY_TEMPLATE_PACK = "tailwind"

Your base template must load Alpine.js and HTMX (not bundled):

<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script src="https://unpkg.com/htmx.org@2.0.0"></script>

Styling

The package templates use custom CSS classes (btn-primary, btn-secondary, btn-important, btn-disabled, form-link, readonly-form-field, toggle), shipped as a Tailwind CSS v4 entry point at static/crispy_crud/crispy_crud.css.

Setup

Run the crispy_crud_css management command, pointing --relative-to at your Tailwind input CSS file, and paste its output into that file after @import "tailwindcss";:

$ python manage.py crispy_crud_css --relative-to theme/static_src/src/styles.css
/* django-crispy-crud — paste after @import "tailwindcss"; */
@import "../../../.venv/lib/python3.13/site-packages/crispy_crud/static/crispy_crud/crispy_crud.css";
@source "../../../.venv/lib/python3.13/site-packages/crispy_tailwind/templates";
@import "tailwindcss";
@import "../../../.venv/lib/python3.13/site-packages/crispy_crud/static/crispy_crud/crispy_crud.css";
@source "../../../.venv/lib/python3.13/site-packages/crispy_tailwind/templates";

The @source line is needed because {% crispy %} forms render crispy-tailwind's template pack, whose utility classes your build must scan to generate CSS for. Paths are printed relative to --relative-to's parent directory (matching how @import/@source resolve); omit the flag to get absolute paths instead.

Requirements

Requires tailwindcss >= 4.1 — earlier 4.x releases ignore an explicit @source that points into a .gitignore'd directory such as a uv-managed .venv (tailwindlabs/tailwindcss#15452).

Theming

Every colour in the component classes routes through a --crud-* CSS custom property with a package default as its fallback:

Variable Default Affects
--crud-btn-primary-text white btn-primary text colour
--crud-btn-primary-bg gray-800 btn-primary background
--crud-btn-primary-bg-hover gray-700 btn-primary background on hover
--crud-btn-secondary-text gray-800 btn-secondary text colour
--crud-btn-secondary-bg white btn-secondary background
--crud-btn-secondary-border gray-300 btn-secondary border colour
--crud-btn-secondary-bg-hover gray-50 btn-secondary background on hover
--crud-btn-important-text white btn-important text colour
--crud-btn-important-bg red-700 btn-important background
--crud-btn-important-bg-hover red-500 btn-important background on hover
--crud-btn-disabled-text gray-500 btn-disabled text colour
--crud-btn-disabled-bg gray-200 btn-disabled background
--crud-form-link-text gray-600 form-link text colour
--crud-form-link-text-hover gray-900 form-link text colour on hover
--crud-readonly-text gray-500 readonly-form-field text colour
--crud-readonly-bg gray-50 readonly-form-field background
--crud-readonly-border gray-400 readonly-form-field border colour
--crud-toggle-bg gray-200 toggle button background (unchecked)
--crud-toggle-bg-checked green-200 toggle button background (checked)

Override a variable in your own CSS to rebrand a component — no fork or package rebuild needed:

:root {
  --crud-btn-primary-bg: var(--color-indigo-600);
  --crud-btn-primary-bg-hover: var(--color-indigo-500);
}

See the test app's static_src/styles.css for a working example.

Caveats

  • Re-run the command after a Python minor version upgrade (the python3.X segment of the path changes) or if the virtualenv moves.
  • The pasted paths must also resolve wherever your production CSS is built (e.g. in Docker) -- this holds as long as that build installs the virtualenv at the project root with the same Python version.
  • Projects with no Tailwind build step aren't supported as a first-class case yet.
  • static/crispy_crud/css/components.css is deprecated and will be removed in a future release; use crispy_crud_css instead.

Usage

View mixins

from django.views.generic import CreateView, UpdateView
from django_filters.views import FilterView
from crispy_crud.views import (
    BaseModelAppMixin, CrispyFormArgsMixin,
    FilterFormMixin, HtmxFilterListMixin,
)

class WidgetListView(BaseModelAppMixin, FilterFormMixin, HtmxFilterListMixin, FilterView):
    model = Widget
    filterset_class = WidgetFilter
    template_name = "myapp/widget_list.html"
    partial_template_name = "myapp/fragments/widget_table.html"
    section = "widget-list"
    create_view_name = "widget-create"
    filter_form_class = WidgetFilterForm

class WidgetUpdateView(BaseModelAppMixin, CrispyFormArgsMixin, UpdateView):
    model = Widget
    form_class = WidgetForm
    template_name = "myapp/widget_detail.html"
    section = "widgets"
    delete_view_name = "widget-delete"
    success_url = reverse_lazy("widget-list")

Form helpers

from django import forms
from crispy_crud.forms import CrispyFormMixin

class WidgetForm(CrispyFormMixin, forms.ModelForm):
    cancel_action = "widget-list"

    class Meta:
        model = Widget
        fields = ["name", "description", "is_active"]

CrispyFormMixin provides a crispy FormHelper, cancel link, and Alpine.js dirty-state submit button out of the box. You need to set the layout in your form's __init__ and include self.button_holder at the end:

def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs)
    self.helper.layout = Layout(
        Field("name"),
        Field("description"),
        Field("is_active"),
        self.button_holder,
    )

Filter forms

Create a filter form with a crispy helper using BaseFilterFormHelper:

from crispy_crud.forms import BaseFilterFormHelper
from crispy_forms.layout import Field, Layout

class WidgetFilterFormHelper(BaseFilterFormHelper):
    q_field_placeholder = "Search widgets..."

    def __init__(self):
        super().__init__()
        self.form_class = self.form_css
        self.layout = Layout(
            self.q_field,
            Field("is_active", wrapper_class="w-40"),
            self.button_holder,
        )

class WidgetFilterForm(forms.Form):
    q = forms.CharField(required=False)
    is_active = forms.BooleanField(required=False)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.helper = WidgetFilterFormHelper()

Set filter_form_class on your list view, and include the HTMX filter form fragment in your list template:

{% include 'crispy_crud/fragments/forms/hx_filter_form.html' with view_name="widget-list" %}

This renders a form that auto-submits via HTMX on input changes -- the search field fires after a 500ms debounce, and select/checkbox fields fire on change. The filter_form_fields template tag automatically generates the correct HTMX trigger selectors for your filter fields.

You also need a django-filter FilterSet on your view:

import django_filters

class WidgetFilter(django_filters.FilterSet):
    q = django_filters.CharFilter(field_name="name", lookup_expr="icontains")

    class Meta:
        model = Widget
        fields = ["q", "is_active"]

Template fragments

The package provides reusable template fragments under crispy_crud/fragments/. Include them in your own templates:

{% include 'crispy_crud/fragments/common/messages.html' %}
{% include 'crispy_crud/fragments/common/page_heading.html' %}
{% include 'crispy_crud/fragments/table/table_heading.html' with text="Name" first_column=True %}
{% include 'crispy_crud/fragments/table/table_cell.html' with text=widget.name first_column=True %}
{% include 'crispy_crud/fragments/table/hx_pagination.html' with page=page_obj %}
{% include 'crispy_crud/fragments/details/delete_action.html' %}
{% include 'crispy_crud/fragments/loaders/loading_spinner.html' %}

Static files

Include the Alpine.js components for form state tracking and pagination:

<script src="{% static 'crispy_crud/js/formState.js' %}"></script>
<script src="{% static 'crispy_crud/js/pagination.js' %}"></script>

Configuration

All settings are optional and have sensible defaults:

Setting Default Purpose
CRISPY_CRUD_PAGINATE_BY 25 Default page size
CRISPY_CRUD_PAGE_SIZE_OPTIONS [10, 25, 50, 100] Page-size pills shown in pagination
CRISPY_CRUD_AUDIT_PAGE_SIZE 25 Rows per page in audit history table

Running the test app

The test app in tests/testapp/ is a working example you can clone as a starting point. To run it locally:

git clone https://github.com/howieweiner/django-crispy-crud.git
cd django-crispy-crud
uv run python manage.py migrate
uv run python manage.py runserver

Then visit http://localhost:8000/widgets/ to see the list view with filtering, pagination, create/update forms, and delete with confirmation modal.

Development

uv run pytest                    # run tests
uv run ruff check src/ tests/    # lint
uv run ruff format src/ tests/   # format

What's included

View mixins (crispy_crud.views)

  • PaginationMixin -- page size from query param, cookie, or config
  • HtmxPaginationMixin -- extends PaginationMixin with HTMX URL push
  • CrispyFormArgsMixin -- injects request into form kwargs
  • BaseAppMixin -- section/heading context
  • BaseModelAppMixin -- auto-generated headings, create/delete actions, related-object protection
  • HtmxFilterListMixin -- switches to partial template on HTMX requests
  • FilterFormMixin -- populates filter form in context
  • AuditLogMixin -- paginated audit history (requires django-auditlog)

Form helpers (crispy_crud.forms)

  • CrispyFormMixin -- crispy FormHelper with cancel link and Alpine.js dirty-state
  • AlpineSubmit -- submit button with Alpine.js attributes
  • BaseFilterFormHelper -- filter form helper with search field and reset link

Model mixins (crispy_crud.models)

  • AuditOnUpdateOnly -- suppresses auditlog on creation, only logs updates

Template fragments (crispy_crud/fragments/)

  • common/ -- messages, alerts, page heading
  • table/ -- pagination, table heading/cell, OOB add button
  • details/ -- delete action with confirmation modal, audit history table
  • forms/ -- filter form, field errors, submit/cancel, read-only fields
  • loaders/ -- HTMX loading spinner and busy indicator

Template tags (crispy_crud_tags)

  • filter_form_fields -- generates HTMX trigger selectors for filter form fields

Metadata

Release files for django-crispy-crud 1.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-crispy-crud 1.1.1
File Size Uploaded
django_crispy_crud-1.1.1.tar.gz 31.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-crispy-crud 1.1.1
File Interpreter ABI Platform
django_crispy_crud-1.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 63.0 kB

Release files / django_crispy_crud-1.1.1.tar.gz

Download URL django_crispy_crud-1.1.1.tar.gz
Size 31.4 kB
Tags Source
SHA-256 checksum
How to use checksums
d7bc93e7be53bec73fb5d97c8a26d5fb2f1dbf8c4854c46530b7ab8b7fb1e1f4
BLAKE2b-256 checksum
How to use checksums
5b2ceaef37c6b1737a659f5c94b20aba7ef12a96f89a8749eafc3bba968d5a74
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 7, 2026.

Transparency log

Release files / django_crispy_crud-1.1.1-py3-none-any.whl

Download URL django_crispy_crud-1.1.1-py3-none-any.whl
Size 31.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c93d3029d3ee7ecb0dd774a8f495c06fcf6a5aac57fc142e82843225a6992347
BLAKE2b-256 checksum
How to use checksums
718af6839f12a986205994cec54e05516f31a673252d366daf2d6219ccf3574f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 7, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.1.1 This release

2 release files

1.1.0

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page