Introduction
While Django Unfold provides extensive built-in form functionality, you can also integrate Django Crispy Forms for more complex form layouts and custom rendering.
Django Crispy Forms integration is optional. Unfold’s built-in widgets cover most use cases without requiring Crispy Forms.
Installation
Install the crispy forms extra:
pip install django-unfold[crispy-forms]
Add to your settings:
INSTALLED_APPS = [
# ...
"unfold" ,
"unfold.contrib.forms" ,
"crispy_forms" ,
"crispy_bootstrap4" , # or your preferred template pack
]
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap4"
CRISPY_TEMPLATE_PACK = "bootstrap4"
Basic Usage
Use Crispy Forms in your admin forms:
from django import forms
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Field, Fieldset, Div
from unfold.widgets import UnfoldAdminTextInputWidget, UnfoldAdminTextareaWidget
class ArticleForm ( forms . ModelForm ):
def __init__ ( self , * args , ** kwargs ):
super (). __init__ ( * args, ** kwargs)
self .helper = FormHelper()
self .helper.layout = Layout(
Fieldset(
'Article Information' ,
Field( 'title' ),
Field( 'slug' ),
Field( 'content' ),
),
Fieldset(
'Metadata' ,
Field( 'author' ),
Field( 'published_date' ),
Field( 'tags' ),
)
)
class Meta :
model = Article
fields = '__all__'
widgets = {
'title' : UnfoldAdminTextInputWidget(),
'content' : UnfoldAdminTextareaWidget(),
}
from django.contrib import admin
from unfold.admin import ModelAdmin
from .forms import ArticleForm
@admin.register (Article)
class ArticleAdmin ( ModelAdmin ):
form = ArticleForm
Advanced Layouts
Multi-Column Layout
Create multi-column form layouts:
from crispy_forms.layout import Layout, Row, Column, Field
class PersonForm ( forms . ModelForm ):
def __init__ ( self , * args , ** kwargs ):
super (). __init__ ( * args, ** kwargs)
self .helper = FormHelper()
self .helper.layout = Layout(
Row(
Column(Field( 'first_name' ), css_class = 'form-group col-md-6' ),
Column(Field( 'last_name' ), css_class = 'form-group col-md-6' ),
),
Row(
Column(Field( 'email' ), css_class = 'form-group col-md-8' ),
Column(Field( 'phone' ), css_class = 'form-group col-md-4' ),
),
Field( 'address' ),
Row(
Column(Field( 'city' ), css_class = 'form-group col-md-4' ),
Column(Field( 'state' ), css_class = 'form-group col-md-4' ),
Column(Field( 'zip_code' ), css_class = 'form-group col-md-4' ),
),
)
class Meta :
model = Person
fields = '__all__'
Create tabbed form interfaces:
from crispy_forms.layout import Layout, TabHolder, Tab, Field
class ProductForm ( forms . ModelForm ):
def __init__ ( self , * args , ** kwargs ):
super (). __init__ ( * args, ** kwargs)
self .helper = FormHelper()
self .helper.layout = Layout(
TabHolder(
Tab(
'Basic Information' ,
Field( 'name' ),
Field( 'sku' ),
Field( 'price' ),
),
Tab(
'Description' ,
Field( 'short_description' ),
Field( 'full_description' ),
),
Tab(
'Images' ,
Field( 'main_image' ),
Field( 'gallery_images' ),
),
Tab(
'SEO' ,
Field( 'meta_title' ),
Field( 'meta_description' ),
Field( 'meta_keywords' ),
),
)
)
class Meta :
model = Product
fields = '__all__'
Accordion Layout
Collapsible sections in forms:
from crispy_forms.layout import Layout, Accordion, AccordionGroup, Field
class SettingsForm ( forms . ModelForm ):
def __init__ ( self , * args , ** kwargs ):
super (). __init__ ( * args, ** kwargs)
self .helper = FormHelper()
self .helper.layout = Layout(
Accordion(
AccordionGroup(
'General Settings' ,
Field( 'site_name' ),
Field( 'site_description' ),
Field( 'admin_email' ),
),
AccordionGroup(
'Email Configuration' ,
Field( 'smtp_host' ),
Field( 'smtp_port' ),
Field( 'smtp_username' ),
Field( 'smtp_password' ),
),
AccordionGroup(
'Social Media' ,
Field( 'facebook_url' ),
Field( 'twitter_url' ),
Field( 'linkedin_url' ),
),
)
)
class Meta :
model = Settings
fields = '__all__'
Custom Field Rendering
Field Wrappers
Customize field wrapper classes:
from crispy_forms.layout import Layout, Field, Div
class CustomForm ( forms . ModelForm ):
def __init__ ( self , * args , ** kwargs ):
super (). __init__ ( * args, ** kwargs)
self .helper = FormHelper()
self .helper.layout = Layout(
Div(
Field( 'title' , css_class = 'custom-input' ),
css_class = 'custom-wrapper'
),
Field( 'description' , wrapper_class = 'description-field' ),
)
Custom Templates
Use custom field templates:
from crispy_forms.layout import Layout, Field
class FormWithCustomTemplate ( forms . ModelForm ):
def __init__ ( self , * args , ** kwargs ):
super (). __init__ ( * args, ** kwargs)
self .helper = FormHelper()
self .helper.layout = Layout(
Field( 'name' , template = 'custom/field_template.html' ),
)
Create the template:
templates/custom/field_template.html
< div class = "custom-field-wrapper" >
< label for = "{{ field.id_for_label }}" class = "custom-label" >
{{ field.label }}
</ label >
{% include 'bootstrap4/field.html' %}
{% if field.help_text %}
< small class = "custom-help-text" > {{ field.help_text }} </ small >
{% endif %}
</ div >
from crispy_forms.layout import Layout, Submit, Button, Reset
class FormWithButtons ( forms . ModelForm ):
def __init__ ( self , * args , ** kwargs ):
super (). __init__ ( * args, ** kwargs)
self .helper = FormHelper()
self .helper.layout = Layout(
Field( 'name' ),
Field( 'email' ),
Div(
Submit( 'submit' , 'Save' , css_class = 'btn-primary' ),
Button( 'cancel' , 'Cancel' , css_class = 'btn-secondary' ),
Reset( 'reset' , 'Reset Form' , css_class = 'btn-warning' ),
css_class = 'form-actions'
)
)
Use Crispy Forms with inline formsets:
from django.forms import inlineformset_factory
from crispy_forms.layout import Layout, Field
from crispy_forms.helper import FormHelper
class BookInlineForm ( forms . ModelForm ):
def __init__ ( self , * args , ** kwargs ):
super (). __init__ ( * args, ** kwargs)
self .helper = FormHelper()
self .helper.layout = Layout(
Field( 'title' ),
Field( 'isbn' ),
Field( 'publication_date' ),
)
self .helper.form_tag = False
class Meta :
model = Book
fields = '__all__'
BookFormSet = inlineformset_factory(
Author,
Book,
form = BookInlineForm,
extra = 1 ,
can_delete = True
)
from django.contrib import admin
from unfold.admin import ModelAdmin
class BookInline ( admin . StackedInline ):
model = Book
form = BookInlineForm
extra = 1
@admin.register (Author)
class AuthorAdmin ( ModelAdmin ):
inlines = [BookInline]
Conditional Display
Combine Crispy Forms with Unfold’s conditional fields:
from crispy_forms.layout import Layout, Field, Div
from unfold.widgets import UnfoldAdminSelectWidget
class ConditionalForm ( forms . ModelForm ):
def __init__ ( self , * args , ** kwargs ):
super (). __init__ ( * args, ** kwargs)
self .helper = FormHelper()
self .helper.layout = Layout(
Field( 'product_type' ),
Div(
Field( 'download_url' ),
css_class = 'conditional-field' ,
x_show = "productType === 'digital'"
),
Div(
Field( 'weight' ),
Field( 'dimensions' ),
css_class = 'conditional-field' ,
x_show = "productType === 'physical'"
),
)
product_type = forms.ChoiceField(
choices = [( 'digital' , 'Digital' ), ( 'physical' , 'Physical' )],
widget = UnfoldAdminSelectWidget( attrs = { 'x-model' : 'productType' })
)
class Meta :
model = Product
fields = '__all__'
Disable automatic form tag generation:
class AdminCompatibleForm ( forms . ModelForm ):
def __init__ ( self , * args , ** kwargs ):
super (). __init__ ( * args, ** kwargs)
self .helper = FormHelper()
self .helper.form_tag = False # Let admin handle the form tag
self .helper.layout = Layout(
Field( 'name' ),
Field( 'description' ),
)
class FormWithAttributes ( forms . ModelForm ):
def __init__ ( self , * args , ** kwargs ):
super (). __init__ ( * args, ** kwargs)
self .helper = FormHelper()
self .helper.form_id = 'custom-form'
self .helper.form_class = 'form-horizontal'
self .helper.form_method = 'post'
self .helper.form_action = '/custom-action/'
Best Practices
Disable Form Tag in Admin
Ensure your custom layouts maintain proper ARIA attributes and labels: Field( 'email' , aria_describedby = 'email-help' )
Test your multi-column layouts on different screen sizes: Column(Field( 'name' ), css_class = 'col-12 col-md-6' )
Limitations
Be aware of these limitations when using Crispy Forms with Unfold:
Template Pack Compatibility : Some template packs may conflict with Unfold’s styling
Widget Override : Crispy Forms may override some Unfold widget features
JavaScript Conflicts : Custom Crispy Forms JavaScript may conflict with Alpine.js
Inline Formsets : Limited support for complex inline formset layouts
Alternative: Use Unfold’s Built-in Features
In most cases, Unfold’s built-in features are sufficient:
from django.contrib import admin
from unfold.admin import ModelAdmin
@admin.register (Product)
class ProductAdmin ( ModelAdmin ):
# Use fieldsets for grouping
fieldsets = (
( 'Basic Information' , {
'fields' : ( 'name' , 'sku' , 'price' )
}),
( 'Description' , {
'fields' : ( 'short_description' , 'full_description' )
}),
)
# Use inlines for related objects
inlines = [VariantInline, ImageInline]
Next Steps
Forms Overview Back to forms system overview
Custom Widgets Explore all available widgets