Django Nitro
Server-rendered Django views with HTMX + Alpine.js - No JavaScript required
Django Nitro is a library of views, template tags, and components for building reactive Django applications using HTMX and Alpine.js. Server renders HTML, HTMX swaps it, Alpine handles local UI.
Why Django Nitro?
- Zero JavaScript - Build reactive UIs with Python views and template tags
- Standard Django - Uses Django views, forms, and templates (no custom runtime)
- HTMX powered - Server-rendered HTML swaps for reactive interactions
- Lightweight - HTMX (~14KB) + Alpine.js (~15KB) via CDN
- Batteries included - Search, filters, pagination, modals, slideovers, toasts, file uploads, wizards
- Multi-tenant ready - Generic
OrganizationMixinfor multi-tenant apps
Table of Contents
- Installation
- Quick Start
- Views
- Template Tags
- Forms
- Components
- Multi-Tenancy
- Utilities
- Migration from v0.7
Installation
pip install django-nitro
Requirements
- Python 3.12+
- Django 5.2+
- HTMX and Alpine.js (loaded via CDN, no pip install needed)
Setup
1. Add to INSTALLED_APPS
# settings.py
INSTALLED_APPS = [
# ...
'nitro',
]
2. Add scripts to your base template
{% load nitro_tags %}
<!DOCTYPE html>
<html>
<head>
{% nitro_scripts %}
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
</head>
<body>
{% block content %}{% endblock %}
{% nitro_toast %}
</body>
</html>
{% nitro_scripts %} includes HTMX, nitro.js, and alpine-components.js.
Quick Start
1. Define the View
# views.py
from nitro.views import NitroListView
class PropertyListView(NitroListView):
model = Property
template_name = 'properties/property_list.html'
partial_template = 'properties/partials/property_list_content.html'
search_fields = ['name', 'address']
filter_fields = ['status', 'property_type']
paginate_by = 20
2. Create the Template
<!-- property_list.html -->
{% load nitro_tags %}
{% extends "base.html" %}
{% block content %}
<div class="flex gap-4 mb-4">
{% nitro_search target='#list-content' %}
{% nitro_filter field='status' options=filter_options.status %}
</div>
<div id="list-content">
{% include "properties/partials/property_list_content.html" %}
</div>
{% endblock %}
<!-- partials/property_list_content.html -->
{% load nitro_tags %}
{% for property in object_list %}
<div class="p-4 border rounded-lg">
<h3>{{ property.name }}</h3>
<p>{{ property.address }}</p>
<span>{{ property.status|status_badge }}</span>
</div>
{% endfor %}
{% nitro_pagination page_obj target='#list-content' %}
3. Wire the URL
# urls.py
from .views import PropertyListView
urlpatterns = [
path('properties/', PropertyListView.as_view(), name='property_list'),
]
That's it. Search with debounce, filters, and pagination all work via HTMX without writing JavaScript.
Views
NitroView
Base view with HTMX detection and toast helpers.
from nitro.views import NitroView
class DashboardView(NitroView):
template_name = 'dashboard.html'
partial_template = 'partials/dashboard_content.html' # for HTMX requests
Methods:
self.is_htmx- Check if request is HTMXself.toast(message, level)- Send toast notificationself.success(message)/self.error(message)- Toast shortcutsself.htmx_redirect(url)- Client-side redirect via HTMXself.htmx_refresh()- Refresh page via HTMX
NitroListView
List view with built-in search, filters, sorting, and pagination.
class TenantListView(NitroListView):
model = Tenant
template_name = 'tenants/list.html'
partial_template = 'tenants/partials/list_content.html'
search_fields = ['name', 'email', 'phone']
filter_fields = ['status']
sortable_fields = ['name', 'created_at']
paginate_by = 25
select_related = ['property']
default_sort = '-created_at'
def get_filter_options(self):
return {
'status': [('active', 'Activo'), ('inactive', 'Inactivo')],
}
NitroFormView
Form handling with HTMX support.
class PropertyFormView(NitroFormView):
form_class = PropertyForm
template_name = 'properties/form.html'
success_url = '/properties/'
NitroCreateView / NitroUpdateView / NitroDeleteView
CRUD views with slideover + toast pattern:
class PropertyCreateView(CompanyMixin, NitroCreateView):
model = Property
form_class = PropertyForm
template_name = 'properties/form.html'
# Automatically: closes slideover, shows toast, refreshes page
class PropertyUpdateView(CompanyMixin, NitroUpdateView):
model = Property
form_class = PropertyForm
template_name = 'properties/form.html'
class PropertyDeleteView(CompanyMixin, NitroDeleteView):
model = Property
def can_delete(self, obj):
if obj.leases.filter(status='active').exists():
return False, 'Has active leases'
return True, None
NitroModelView
Detail view for a single model instance.
class PropertyDetailView(NitroModelView):
model = Property
template_name = 'properties/detail.html'
NitroWizard
Multi-step form wizard with session-based data persistence.
from nitro.wizards import NitroWizard, WizardStep
class OnboardingWizard(NitroWizard):
wizard_name = 'onboarding'
steps = [
WizardStep('company', CompanyForm, 'wizard/company.html', 'Company'),
WizardStep('plan', PlanForm, 'wizard/plan.html', 'Plan'),
WizardStep('confirm', None, 'wizard/confirm.html', 'Confirm'),
]
def done(self, wizard_data):
Company.objects.create(**wizard_data['company'])
self.clear_wizard_data()
return redirect('dashboard')
Template Tags
Load with {% load nitro_tags %}.
HTMX Action Tags
{% nitro_search target='#list' placeholder='Search...' %}
{% nitro_filter field='status' options=opts target='#list' %}
{% nitro_pagination page_obj target='#list' %}
{% nitro_sort 'name' 'Name' target='#list' %}
{% nitro_delete url=delete_url target='#list' confirm='Delete?' %}
Form Tags
{% nitro_field form.name %}
{% nitro_select form.category search_url='/api/search/' %}
{% nitro_form_footer slideover='create-item' label='Save' %}
Component Tags
{% nitro_modal id='create' title='New Item' %}...{% end_nitro_modal %}
{% nitro_slideover id='edit' title='Edit' size='lg' %}...{% end_nitro_slideover %}
{% nitro_tabs id='tabs' target='#content' %}
{% nitro_tab name='info' label='Info' active=True %}
{% nitro_tab name='docs' label='Documents' %}
{% end_nitro_tabs %}
{% nitro_empty_state icon='icon' title='No data' message='Add your first item' %}
{% nitro_stats_card icon='icon' label='Revenue' value=total change='+12%' change_type='positive' %}
{% nitro_avatar user size='md' %}
{% nitro_file_upload upload_url='/upload/' field_name='file' accept='image/*' multiple=True %}
{% nitro_toast %}
UI Helpers
{% nitro_open_modal 'create' %} {# attrs for a button to open modal #}
{% nitro_close_modal 'create' %}
{% nitro_open_slideover 'edit' %}
{% nitro_close_slideover 'edit' %}
{% nitro_scripts %} {# include HTMX + Nitro JS #}
Transition Presets
<div x-show="open" {% nitro_transition 'fade' %}>...</div>
<div x-show="open" {% nitro_transition 'slide-up' '200' %}>...</div>
<div x-show="open" {% nitro_transition 'scale' %}>...</div>
Presets: fade, slide-up, slide-down, slide-right, slide-left, scale.
Keyboard Shortcuts
<body {% nitro_key 'meta.k' "$dispatch('focus-search')" %}>
<div {% nitro_key 'escape' 'open = false' %}>
Display Filters
{{ amount|currency }} {# RD$ 15,000.00 #}
{{ amount|currency:'USD' }} {# US$ 15,000.00 #}
{{ item.status|status_badge }} {# colored badge #}
{{ ticket.priority|priority_badge }} {# colored badge #}
{{ phone|phone_format }} {# (809) 555-1234 #}
{{ date|relative_date }} {# Hoy, Ayer, Hace 3 dias #}
{{ uuid|truncate_id:8 }} {# first 8 chars #}
{{ 4.5|rating }} {# star rating SVG #}
{{ count|pluralize_es:'item,items' }} {# Spanish pluralization #}
Forms
from nitro.forms import NitroModelForm, NitroForm, PhoneField, CedulaField, CurrencyField
class PropertyForm(NitroModelForm):
class Meta:
model = Property
fields = ['name', 'address', 'rent_amount']
# All widgets get Tailwind classes automatically
class ContactForm(NitroForm):
phone = PhoneField()
cedula = CedulaField()
amount = CurrencyField()
Components (Alpine.js)
Registered automatically via alpine-components.js:
loadingBtn- Spinner on submit during HTMX requestsfileUpload- Drag-and-drop file uploadsclipboard- Copy to clipboardcharCounter- Character counter for textareasconfirmAction- Confirm dialogtoggle- Collapsible sectionstabs- Client-side tabscurrencyInput- Auto-format currencyphoneInput- Auto-format phone numbersdirtyForm- Unsaved changes warning
Multi-Tenancy
from nitro.mixins import OrganizationMixin
class CompanyMixin(OrganizationMixin):
org_field = 'company'
def get_organization(self):
return self.request.user.company
# Use in views:
class PropertyListView(CompanyMixin, NitroListView):
model = Property # automatically filtered by company
Utilities
Currency
from nitro.utils import format_currency, parse_currency
format_currency(1234.5) # "RD$ 1,234.50"
format_currency(1234.5, 'USD') # "US$ 1,234.50"
parse_currency('RD$ 1,234.50') # Decimal('1234.50')
Dates
from nitro.utils import today, relative_date, month_name, is_overdue, add_months
relative_date(some_date) # "Hoy", "Ayer", "Hace 3 dias"
month_name(1) # "Enero"
is_overdue(due_date) # True/False
add_months(date, 3) # date + 3 months
Configuration
# settings.py (all optional)
NITRO = {
'TOAST_ENABLED': True,
'TOAST_POSITION': 'top-right',
'TOAST_DURATION': 3000,
'DEBUG': False,
}
Migration from v0.7
v0.8.0 is a complete rewrite. The component-based architecture (NitroComponent, Pydantic state, Django Ninja API) has been replaced with standard Django views + HTMX.
What changed
| v0.7 | v0.8 |
|---|---|
NitroComponent classes |
Django views (NitroListView, NitroFormView) |
Pydantic BaseModel state |
Django forms and template context |
| Django Ninja JSON API | Server-rendered HTML + HTMX swaps |
@register_component |
Standard Django URL routing |
call('action') in templates |
hx-post/hx-get HTMX attributes |
nitro_model two-way binding |
Alpine x-model (local) + form submit |
nitro_action template tag |
HTMX attributes via template tags |
requirements: pydantic, django-ninja |
requirements: Django only |
Why the change
The component system added complexity (Pydantic state sync, JSON API, client-side rendering) that wasn't necessary for most Django apps. HTMX achieves the same reactive UX with standard Django patterns, is easier to debug, and has zero additional Python dependencies.
License
MIT License. See LICENSE for details.
Metadata
Release files for django-nitro 0.8.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| django_nitro-0.8.0.tar.gz | 87.9 kB | Without provenance | Details for django_nitro-0.8.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| django_nitro-0.8.0-py3-none-any.whl | Python 3 | none | any | Details for django_nitro-0.8.0-py3-none-any.whl |
Total release size: 193.7 kB
Release files / django_nitro-0.8.0.tar.gz
| Download URL | django_nitro-0.8.0.tar.gz |
|---|---|
| Size | 87.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b75724ac941c394b504c0d06cd36c3d23dd3dfcdaef518826deebca5e11564da
|
|
BLAKE2b-256 checksum How to use checksums |
9df6b986af980149def2fc5d0830cc8e097e715614415d212056d31375868898
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.2
|
Release files / django_nitro-0.8.0-py3-none-any.whl
| Download URL | django_nitro-0.8.0-py3-none-any.whl |
|---|---|
| Size | 105.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
dfab13d2007ce3df75f4125207b98ce17ebf7370b02694db527858c53c0097fd
|
|
BLAKE2b-256 checksum How to use checksums |
39abd65ed1c94394c4ff96c79ed8d1a6512b7d97699e56c0bbeff216d1669047
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.2
|
Security for django-nitro 0.8.0
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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.