djust is a powerful framework that brings Phoenix LiveView-style real-time interactivity to Django, powered by Rust's blazing-fast VDOM diffing.
Server-rendered UI that updates in real-time via WebSockets. No JavaScript frameworks required.
Leverages Rust's performance for efficient VDOM diffing and minimal data transfer.
Use with Bootstrap 5, Tailwind CSS, or plain HTML. Your choice.
Rich set of built-in components: buttons, alerts, forms, tables, modals, and more.
Seamless integration with Django's form system for validation and processing.
Write all your logic in Python. No need to learn a frontend framework.
When building modern reactive UIs, Django developers traditionally face two difficult choices:
Rust powers the performance-critical rendering path, delivering unprecedented speed:
| Operation | Django | djust | Speedup |
|---|---|---|---|
| Template Rendering (100 items) | 2.5 ms | 0.15 ms | 16.7x faster |
| Large List (10k items) | 450 ms | 12 ms | 37.5x faster |
| Virtual DOM Diff | N/A | 0.08 ms | Sub-millisecond |
| Round-trip Update | 50 ms | 5 ms | 10x faster |
Unlike Phoenix LiveView or JavaScript frameworks, you keep everything Django offers:
No equivalent in other frameworks
Rich, mature, battle-tested
Complete authentication system
DRF, Celery, Allauth, and more
Stay in Python - no context switching between languages:
class TodoListView(LiveView):
template_name = 'todos.html'
def mount(self, request):
self.todos = []
def add_todo(self, text=""):
self.todos.append({
'text': text,
'done': False
})
def toggle_todo(self, index=0):
self.todos[index]['done'] = \\
not self.todos[index]['done']
✅ One language, 50-70% less code
# Django backend
class TodoViewSet(viewsets.ModelViewSet):
queryset = Todo.objects.all()
serializer_class = TodoSerializer
// React frontend
function TodoList() {
const [todos, setTodos] = useState([]);
const addTodo = async (text) => {
const res = await fetch('/api/todos/', {
method: 'POST',
body: JSON.stringify({ text })
});
const todo = await res.json();
setTodos([...todos, todo]);
};
// More boilerplate...
}
❌ Two codebases, complex state sync
# djust - Simple!
pip install djust
# Add to INSTALLED_APPS
# Write code, refresh browser - done! ✅
# vs React/Vue/Svelte - Complex
npm install react webpack babel ...
# Configure webpack.config.js, babel.config.js, tsconfig.json...
npm run build # Wait 30-60 seconds
# Hope build doesn't break in production ❌
Client bundle: Just ~5KB JavaScript (vs 100-300KB+ for React apps)
Real-time metrics, live data updates, interactive filtering and search.
Real-time inventory, live shopping cart, instant search, dynamic filters.
Real-time validation, multi-step wizards, dynamic fields, instant feedback.
Real-time updates, live notifications, shared state visualization.
Interactive data management, live search and filtering, instant updates.
Live charts and graphs, interactive filters, real-time data feeds.
Add reactivity to specific components without rewriting your entire app:
# Keep existing Django views
class ProductListView(ListView):
model = Product
template_name = 'products/list.html'
# Add live filtering to just the filters widget
class LiveProductFilters(LiveView):
def mount(self, request):
self.search = ""
self.categories = Category.objects.all()
def filter_products(self, value=""):
self.search = value
# Real-time filtering without page reload!
The secret: Rust handles the performance-critical path
┌─────────────────────────────────────────────┐
│ Developer writes Python (high productivity)│
│ class MyView(LiveView): │
│ def increment(self): │
│ self.count += 1 │
└──────────────────┬──────────────────────────┘
│ Python/Rust FFI (PyO3)
┌──────────────────▼──────────────────────────┐
│ Rust executes hot path (high performance) │
│ - Template parsing & rendering │
│ - VDOM construction & diffing │
│ - HTML parsing │
│ - Binary serialization (MessagePack) │
└─────────────────────────────────────────────┘
│ WebSocket (Binary)
┌──────────────────▼──────────────────────────┐
│ Browser receives minimal updates │
│ - Only changed DOM nodes │
│ - ~5KB client JavaScript │
│ - Instant UI updates │
└─────────────────────────────────────────────┘
pip install djust
# settings.py
INSTALLED_APPS = [
# ... other apps
'djust',
]
# asgi.py
import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from djust.routing import websocket_urlpatterns
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": AuthMiddlewareStack(
URLRouter(websocket_urlpatterns)
),
})
# settings.py
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels.layers.InMemoryChannelLayer"
}
}
# For production, use Redis:
# CHANNEL_LAYERS = {
# "default": {
# "BACKEND": "channels_redis.core.RedisChannelLayer",
# "CONFIG": {
# "hosts": [("127.0.0.1", 6379)],
# },
# },
# }
# Development
python manage.py runserver
# Production with Daphne
daphne -b 0.0.0.0 -p 8000 myproject.asgi:application
# Production with Uvicorn
uvicorn myproject.asgi:application --host 0.0.0.0 --port 8000
Create your first LiveView in 5 minutes:
# views.py
from djust import LiveView
from djust.components import ButtonComponent, AlertComponent
class CounterView(LiveView):
template_name = "counter.html"
def mount(self, request):
"""Initialize state when page loads"""
self.count = 0
self.message = ""
# Create interactive button
self.increment_btn = ButtonComponent(
label="Increment",
variant="primary",
on_click="increment"
)
def increment(self):
"""Handle button click"""
self.count += 1
self.message = f"Count is {self.count}"
<!-- templates/counter.html -->
{% extends "base.html" %}
{% block content %}
<div data-liveview-root>
<h1>Count: {{ count }}</h1>
{{ increment_btn.render }}
{% if message %}
<p class="text-success">{{ message }}</p>
{% endif %}
</div>
{% endblock %}
# urls.py
from django.urls import path
from .views import CounterView
urlpatterns = [
path('counter/', CounterView.as_view(), name='counter'),
]
/counter/ and click the button. The count updates in real-time without page reload!
djust uses a server-centric architecture:
Browser Server
-------- --------
1. User visits /page/ → Django renders initial HTML
2. Browser receives HTML ← + LiveView JavaScript
3. WebSocket connects ↔ WebSocket handler
4. User clicks button → Event sent via WS
5. Server processes ← Python method executed
6. VDOM diff calculated ← Rust computes minimal changes
7. DOM updated ← Only changes sent via WS
data-liveview-rootEach URL route has one LiveView class that manages the entire page. Within that LiveView, you can:
class MyView(LiveView):
template_name = "my_template.html"
def mount(self, request):
"""Called once when page first loads
Initialize state and components here"""
self.counter = 0
self.user = request.user
self.items = []
def get_context_data(self, **kwargs):
"""Called on every render
Add computed values to template context"""
context = super().get_context_data(**kwargs)
context['item_count'] = len(self.items)
context['user_name'] = self.user.get_full_name()
return context
def handle_connect(self, request):
"""Called when WebSocket connects
Optional: customize connection handling"""
pass
def handle_disconnect(self):
"""Called when WebSocket disconnects
Optional: cleanup resources"""
pass
Any instance attribute becomes available in your template:
def mount(self, request):
# These are all accessible in templates as {{ variable_name }}
self.counter = 0
self.message = "Hello"
self.items = ["a", "b", "c"]
self.user_data = {"name": "John", "age": 30}
self.is_active = True
Define methods to handle user interactions:
def increment(self):
"""Called when element with @click="increment" is clicked"""
self.counter += 1
def handle_submit(self):
"""Called when form with @submit="handle_submit" is submitted"""
# Access form data via self.form_data
query = self.form_data.get('query', '')
self.results = search(query)
def delete_item(self, item_id):
"""Called with data-item-id parameter"""
self.items = [i for i in self.items if i.id != int(item_id)]
from djust.components import ButtonComponent
self.my_button = ButtonComponent(
label="Click Me",
variant="primary", # primary, secondary, success, danger, warning, info
size="md", # sm, md, lg
on_click="handle_click",
icon="plus", # Bootstrap icon name
disabled=False
)
from djust.components import AlertComponent
self.alert = AlertComponent(
message="Operation successful!",
variant="success", # success, info, warning, danger
dismissible=True,
icon="check-circle"
)
from djust.components import CardComponent
self.card = CardComponent(
title="User Profile",
content="Profile content here
",
footer="Last updated: Today",
variant="light", # light, dark, primary, etc.
image_url="/static/img/profile.jpg"
)
from djust.components import TableComponent
self.table = TableComponent(
columns=[
{"key": "name", "label": "Name", "sortable": True},
{"key": "email", "label": "Email"},
{"key": "created", "label": "Created", "sortable": True},
],
data=users,
striped=True,
hover=True,
on_row_click="view_user"
)
from djust.components import ModalComponent
self.modal = ModalComponent(
title="Confirm Delete",
content="Are you sure you want to delete this item?",
size="md", # sm, md, lg, xl
show=False, # Control visibility
on_confirm="confirm_delete",
on_cancel="close_modal"
)
See the Component Kitchen Sink for examples of all available components.
Components automatically receive stable component_id values based on their attribute names. This eliminates manual ID management:
# When you write:
self.alert_success = AlertComponent(message="Success!")
# The framework automatically:
# 1. Sets component.component_id = "alert_success"
# 2. Persists this ID across renders and events
# 3. Uses it in HTML: data-component-id="alert_success"
# 4. Routes events back to the correct component
alert_success) is already unique within your viewEvent handlers receive the component_id automatically and can use it to route to the correct component:
class MyView(LiveView):
def mount(self, request):
self.alert_warning = AlertComponent(
message="Warning message",
dismissible=True
)
# component_id is automatically "alert_warning"
def dismiss(self, component_id: str = None):
"""Handle dismissal - automatically routes to correct component"""
if component_id and hasattr(self, component_id):
component = getattr(self, component_id)
if hasattr(component, 'dismiss'):
component.dismiss() # component_id="alert_warning"
When the dismiss button is clicked, the client automatically sends component_id="alert_warning", and the handler uses getattr(self, "alert_warning") to find the component.
Attach event handlers using special @ attributes:
| Directive | Description | Example |
|---|---|---|
@click |
Handle click events | <button @click="increment"> |
@submit |
Handle form submissions | <form @submit="handle_submit"> |
@change |
Handle input changes | <input @change="validate_field"> |
@input |
Handle input while typing | <input @input="live_search"> |
Pass data to event handlers using data-* attributes:
<!-- Template -->
<button
@click="delete_item"
data-item-id="{{ item.id }}"
data-confirm="true"
>
Delete
</button>
# View
def delete_item(self, item_id, confirm=None):
"""Receives parameters from data-* attributes"""
if confirm == "true":
self.items = [i for i in self.items if i.id != int(item_id)]
Access form field values in submit handlers:
def handle_submit(self):
# Form data automatically available in self.form_data
username = self.form_data.get('username', '')
password = self.form_data.get('password', '')
remember = self.form_data.get('remember_me', False)
# Process the data
if authenticate(username, password):
self.success_message = "Login successful!"
djust seamlessly integrates with Django's form system for validation and processing.
# forms.py
from django import forms
class ContactForm(forms.Form):
name = forms.CharField(max_length=100)
email = forms.EmailField()
message = forms.CharField(widget=forms.Textarea)
# views.py
from djust import LiveView
from .forms import ContactForm
class ContactFormView(LiveView):
template_name = "contact.html"
def mount(self, request):
self.form = ContactForm()
self.success_message = ""
def handle_submit(self):
# Create form instance with submitted data
self.form = ContactForm(self.form_data)
if self.form.is_valid():
# Process the data
name = self.form.cleaned_data['name']
email = self.form.cleaned_data['email']
message = self.form.cleaned_data['message']
# Send email, save to DB, etc.
send_mail(name, email, message)
self.success_message = "Thank you! We'll be in touch."
self.form = ContactForm() # Reset form
# Form errors automatically displayed in template
Use the auto_form template tag for automatic form rendering:
<!-- templates/contact.html -->
{% load djust %}
<div data-liveview-root>
<form @submit="handle_submit">
{% auto_form form framework="bootstrap5" %}
<button type="submit" class="btn btn-primary">Submit</button>
</form>
{% if success_message %}
<div class="alert alert-success">{{ success_message }}</div>
{% endif %}
</div>
The auto_form tag supports multiple CSS frameworks:
<!-- Bootstrap 5 (default) -->
{% auto_form form framework="bootstrap5" %}
<!-- Tailwind CSS -->
{% auto_form form framework="tailwind" %}
<!-- Plain HTML -->
{% auto_form form framework="plain" %}
Validate fields as users type:
class SignupFormView(LiveView):
def mount(self, request):
self.form = SignupForm()
self.username_error = ""
def validate_username(self):
# Called when username field changes
username = self.form_data.get('username', '')
if len(username) < 3:
self.username_error = "Username must be at least 3 characters"
elif User.objects.filter(username=username).exists():
self.username_error = "Username already taken"
else:
self.username_error = ""
<input
type="text"
name="username"
@change="validate_username"
class="form-control"
>
{% if username_error %}
<div class="text-danger">{{ username_error }}</div>
{% endif %}
See the Forms Demo for complete examples of form integration.
All LiveView templates must have a root element with data-liveview-root:
{% extends "base.html" %}
{% block content %}
<div data-liveview-root>
<!-- Your LiveView content here -->
<h1>{{ title }}</h1>
<p>{{ message }}</p>
</div>
{% endblock %}
All instance attributes from your LiveView are available in templates:
# views.py
def mount(self, request):
self.counter = 0
self.items = ["apple", "banana", "cherry"]
self.user = request.user
self.config = {"theme": "dark", "notifications": True}
<!-- templates/my_view.html -->
<p>Count: {{ counter }}</p>
<ul>
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
</ul>
<p>Welcome, {{ user.username }}!</p>
<p>Theme: {{ config.theme }}</p>
Components are rendered using the .render attribute:
# views.py
def mount(self, request):
self.save_btn = ButtonComponent(
label="Save",
variant="success",
on_click="save_data"
)
self.alert = AlertComponent(
message="Changes saved!",
variant="success"
)
<!-- Render components -->
{{ save_btn.render }}
{{ alert.render }}
<!-- Show/hide based on state -->
{% if is_loading %}
<div class="spinner">Loading...</div>
{% else %}
<div class="content">{{ data }}</div>
{% endif %}
<!-- Render list items -->
{% for item in items %}
<div class="item">
<h3>{{ item.title }}</h3>
<button @click="delete" data-item-id="{{ item.id }}">Delete</button>
</div>
{% empty %}
<p>No items found.</p>
{% endfor %}
djust provides custom template tags:
{% load djust %}
<!-- Auto-render Django forms -->
{% auto_form form framework="bootstrap5" %}
<!-- Component rendering helpers -->
{% render_component component %}
State is managed as instance attributes on your LiveView class:
class TodoView(LiveView):
def mount(self, request):
# Initialize state
self.todos = []
self.new_todo = ""
self.filter = "all" # all, active, completed
def add_todo(self):
# Modify state - UI updates automatically
if self.new_todo.strip():
self.todos.append({
"id": len(self.todos) + 1,
"text": self.new_todo,
"completed": False
})
self.new_todo = "" # Reset input
def toggle_todo(self, todo_id):
# Update nested state
for todo in self.todos:
if todo["id"] == int(todo_id):
todo["completed"] = not todo["completed"]
def delete_todo(self, todo_id):
# Remove from state
self.todos = [t for t in self.todos if t["id"] != int(todo_id)]
Use get_context_data for computed values:
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Compute derived values
context['active_count'] = sum(1 for t in self.todos if not t['completed'])
context['completed_count'] = sum(1 for t in self.todos if t['completed'])
context['filtered_todos'] = self._get_filtered_todos()
return context
def _get_filtered_todos(self):
if self.filter == "active":
return [t for t in self.todos if not t['completed']]
elif self.filter == "completed":
return [t for t in self.todos if t['completed']]
return self.todos
State can be persisted to database, session, or cache:
class ShoppingCartView(LiveView):
def mount(self, request):
# Load from session
self.cart_items = request.session.get('cart', [])
def add_to_cart(self, product_id):
self.cart_items.append({"id": product_id, "quantity": 1})
# Save to session
self.request.session['cart'] = self.cart_items
def handle_disconnect(self):
# Save to database on disconnect
if self.request.user.is_authenticated:
Cart.objects.update_or_create(
user=self.request.user,
defaults={'items': self.cart_items}
)
Any state change automatically triggers a re-render:
def increment(self):
self.counter += 1 # UI updates automatically
def load_data(self):
self.is_loading = True # Shows loading spinner
self.data = fetch_from_api()
self.is_loading = False # Hides spinner, shows data
Create reusable components by inheriting from LiveComponent:
# components.py
from djust.components import LiveComponent
class UserCardComponent(LiveComponent):
"""Reusable user profile card"""
template_name = "components/user_card.html"
def __init__(self, user, show_actions=True):
super().__init__()
self.user = user
self.show_actions = show_actions
self.is_following = False
def toggle_follow(self):
"""Handle follow/unfollow action"""
self.is_following = not self.is_following
if self.is_following:
Follow.objects.create(follower=self.current_user, following=self.user)
else:
Follow.objects.filter(follower=self.current_user, following=self.user).delete()
<!-- templates/components/user_card.html -->
<div class="card">
<img src="{{ user.avatar_url }}" class="card-img-top" alt="Avatar">
<div class="card-body">
<h5 class="card-title">{{ user.get_full_name }}</h5>
<p class="card-text">{{ user.bio }}</p>
{% if show_actions %}
<button
@click="toggle_follow"
class="btn btn-{% if is_following %}secondary{% else %}primary{% endif %}"
>
{% if is_following %}Unfollow{% else %}Follow{% endif %}
</button>
{% endif %}
</div>
</div>
# views.py
from .components import UserCardComponent
class ProfileView(LiveView):
template_name = "profile.html"
def mount(self, request):
users = User.objects.all()[:5]
# Create component instances
self.user_cards = [
UserCardComponent(user=user, show_actions=True)
for user in users
]
<!-- templates/profile.html -->
<div data-liveview-root>
<h1>Suggested Users</h1>
<div class="row">
{% for card in user_cards %}
<div class="col-md-4">
{{ card.render }}
</div>
{% endfor %}
</div>
</div>
class MyComponent(LiveComponent):
def mount(self):
"""Called when component is created"""
self.initialize_state()
def update(self, **kwargs):
"""Called when component receives new props"""
self.user = kwargs.get('user')
self.refresh_data()
def destroy(self):
"""Called when component is removed"""
self.cleanup()
djust works with any CSS framework:
<!-- base.html -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Components use Bootstrap classes by default -->
{{ button.render }} <!-- Renders with btn, btn-primary, etc. -->
<!-- base.html -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Use Tailwind classes in templates -->
<div class="bg-blue-500 text-white p-4 rounded-lg">
{{ content }}
</div>
<!-- Add custom styles in your template -->
{% block extra_styles %}
.my-custom-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
padding: 2rem;
color: white;
}
.pulse-animation {
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
{% endblock %}
Apply classes conditionally based on state:
<button
class="btn {% if is_active %}btn-primary{% else %}btn-secondary{% endif %}"
@click="toggle"
>
Toggle
</button>
<div class="item {% if item.completed %}completed{% endif %} {% if item.important %}important{% endif %}">
{{ item.title }}
</div>
Customize built-in components:
self.button = ButtonComponent(
label="Custom Button",
variant="primary",
css_class="my-custom-class shadow-lg", # Additional classes
style="min-width: 200px;" # Inline styles
)
For production deployment, you need:
# settings.py
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("redis", 6379)],
"capacity": 1500,
"expiry": 10,
},
},
}
# nginx.conf
upstream django {
server web:8000;
}
server {
listen 80;
server_name example.com;
# WebSocket support
location /ws/ {
proxy_pass http://django;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 86400;
}
# Regular HTTP
location / {
proxy_pass http://django;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# Static files
location /static/ {
alias /app/static/;
}
}
# docker-compose.yml
version: '3.8'
services:
redis:
image: redis:7-alpine
ports:
- "6379:6379"
web:
build: .
command: daphne -b 0.0.0.0 -p 8000 myproject.asgi:application
volumes:
- .:/app
ports:
- "8000:8000"
depends_on:
- redis
environment:
- DJANGO_SETTINGS_MODULE=myproject.settings.production
- REDIS_HOST=redis
# .env
DEBUG=False
SECRET_KEY=your-secret-key-here
ALLOWED_HOSTS=example.com,www.example.com
REDIS_HOST=redis
DATABASE_URL=postgres://user:pass@db:5432/dbname
# Install Daphne
pip install daphne
# Run production server
daphne -b 0.0.0.0 -p 8000 myproject.asgi:application
# Install Uvicorn
pip install uvicorn[standard]
# Run production server
uvicorn myproject.asgi:application --host 0.0.0.0 --port 8000 --workers 4
| Method/Attribute | Description |
|---|---|
template_name |
Path to the template file |
mount(request) |
Initialize state when page first loads |
get_context_data(**kwargs) |
Add computed values to template context |
handle_connect(request) |
Called when WebSocket connects |
handle_disconnect() |
Called when WebSocket disconnects |
form_data |
Dictionary of form field values from events |
request |
Django request object |
| Method/Attribute | Description |
|---|---|
template_name |
Path to the component template file |
mount() |
Called when component is created |
update(**kwargs) |
Called when component receives new props |
destroy() |
Called when component is removed |
render |
Property that returns rendered HTML |
| Component | Key Parameters |
|---|---|
ButtonComponent |
label, variant, size, on_click, icon, disabled |
AlertComponent |
message, variant, dismissible, icon |
CardComponent |
title, content, footer, variant, image_url |
TableComponent |
columns, data, striped, hover, on_row_click |
ModalComponent |
title, content, size, show, on_confirm, on_cancel |
BadgeComponent |
text, variant, pill |
ProgressComponent |
value, max, variant, striped, animated |
| Directive | Trigger | Example |
|---|---|---|
@click |
Element clicked | <button @click="increment"> |
@submit |
Form submitted | <form @submit="handle_form"> |
@change |
Input value changed | <select @change="filter"> |
@input |
Input while typing | <input @input="search"> |
| Tag | Description | Example |
|---|---|---|
{% auto_form %} |
Auto-render Django forms | {% auto_form form framework="bootstrap5" %} |
{% render_component %} |
Render a component | {% render_component card %} |