Server-rendered Django views with HTMX + Alpine.js - No JavaScript required
Project description
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_nitro-0.8.0.tar.gz.
File metadata
- Download URL: django_nitro-0.8.0.tar.gz
- Upload date:
- Size: 87.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b75724ac941c394b504c0d06cd36c3d23dd3dfcdaef518826deebca5e11564da
|
|
| MD5 |
271ef00805f79c6c8c6602f450d5c20f
|
|
| BLAKE2b-256 |
9df6b986af980149def2fc5d0830cc8e097e715614415d212056d31375868898
|
File details
Details for the file django_nitro-0.8.0-py3-none-any.whl.
File metadata
- Download URL: django_nitro-0.8.0-py3-none-any.whl
- Upload date:
- Size: 105.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dfab13d2007ce3df75f4125207b98ce17ebf7370b02694db527858c53c0097fd
|
|
| MD5 |
71683f31fbd3c53f19a1206f402f6c0d
|
|
| BLAKE2b-256 |
39abd65ed1c94394c4ff96c79ed8d1a6512b7d97699e56c0bbeff216d1669047
|