Django + Lit + Vite: Template Setup and Hot-Reload Workflow
This post walks you through how I set up a hypothetical Django app called acmefront
to work with the Lit JavaScript framework and Vite. I’ll also show you a simple, framework-agnostic hot-reload workflow that makes development a breeze. Just a heads-up: all the names and paths I’m using here are just for illustration.
Step 1: Wiring Up the App, Template, URL, and View
First things first, let’s get the basic Django pieces in place. The app exposes a simple index view and a URL to go with it:
from django.shortcuts import render
def index(request):
return render(request, 'acmefront/index.html')
from django.urls import include, path
from . import views
urlpatterns = [
path('', views.index, name='acmefront-index'),
]
- The template loads Vite-built assets and renders Lit components:
{% load static %}
<!DOCTYPE html>
<html lang="en" {% if debug %}data-env="development"{% endif %}>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Acme Front - Lit Components</title>
<link rel="stylesheet" href="{% static 'acmefront/dist/main.css' %}">
<link rel="icon" href="{% static 'acmefront/favicon.ico' %}" type="image/x-icon">
<!-- Optional: add a small dev indicator so you know you're on dev -->
<style>
.dev-indicator { position: fixed; top: 8px; right: 8px; background: #111; color: #fff; padding: 4px 8px; font-size: 12px; border-radius: 4px; z-index: 9999; }
</style>
</head>
<body>
<div class="dev-indicator">🔥 DEV + Lit</div>
<acme-container title="Demo Widgets"></acme-container>
<script type="module" src="{% static 'acmefront/dist/main.js' %}"></script>
</body>
</html>
Result: Django serves a normal template; Lit web components are delivered via Vite’s bundled main.js
and main.css
in static/acmefront/dist/
and rendered as custom elements in the HTML.
Step 2: Structuring the Lit Components
With the Django side sorted, let’s look at the Lit components. I write them in TypeScript and register them as custom elements (like acme-container
, widget-card
, etc.). I also like to use a shared BaseComponent
that extends LitElement
for any common styles or utilities:
import { LitElement, css } from 'lit';
export class BaseComponent extends LitElement {
static sharedStyles = css`
:host { /* design tokens omitted for brevity */ }
`;
}
- The Vite entrypoint imports the dev refresher, styles, and all components:
// src/main.ts
import './dev-refresh';
import './style.css';
import './components/acme-container';
import './components/widget-card';
import './components/widget-list';
Step 3: Configuring Vite for Django and Lit
Now, let’s get Vite configured to play nicely with our Django and Lit setup. The goal is to have Vite emit assets into the Django static tree and to use modern targets that work well with Lit:
// vite.config.js
import { defineConfig } from 'vite'
import { resolve } from 'path'
export default defineConfig({
build: {
outDir: 'static/acmefront/dist',
rollupOptions: {
input: { main: resolve(__dirname, 'src/main.ts') },
output: {
entryFileNames: '[name].js',
chunkFileNames: '[name].js',
assetFileNames: '[name].[ext]'
}
},
emptyOutDir: true,
manifest: true,
target: 'es2020'
},
server: {
port: 5173,
host: 'localhost',
cors: true,
hmr: { port: 5173 },
watch: { usePolling: true, include: ['templates/**/*.html'] }
},
optimizeDeps: { include: ['lit'] }
})
package.json
scripts:
{
"scripts": {
"dev": "vite",
"build": "vite build",
"watch": "vite build --watch",
"preview": "vite preview",
"type-check": "tsc --noEmit"
},
"dependencies": { "lit": "^3.0.0" }
}
Result: Running npm run build
produces main.js
/main.css
ready for {% static %}
references from Django templates.
Step 4: Setting Up Hot-Reloading
Instead of wrestling with Vite’s native HMR inside Django, I prefer a simpler, framework-agnostic refresh mechanism. Here’s how it works.
A browser polling script (src/dev-refresh.ts
), which runs only in development, polls a static “marker” file for changes:
class DevRefresher {
private lastRefreshTime = 0;
private refreshFile = '/static/acmefront/refresh.txt';
private pollInterval = 1000;
constructor() { if (this.isDevelopment()) this.startPolling(); }
private isDevelopment() { return ['localhost','127.0.0.1'].includes(window.location.hostname) || window.location.hostname.includes('dev'); }
private async checkForChanges() {
const res = await fetch(this.refreshFile + '?_=' + Date.now(), { cache: 'no-cache' });
if (res.ok) {
const t = parseFloat(await res.text());
if (this.lastRefreshTime && t > this.lastRefreshTime) window.location.reload();
else this.lastRefreshTime = t;
}
}
private startPolling() { this.checkForChanges(); setInterval(() => this.checkForChanges(), this.pollInterval); }
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => new DevRefresher()); else new DevRefresher();
- A Django management command wraps Vite building and touches the refresh marker after each successful build, so the browser knows to reload:
# management/commands/vite_dev.py
class ViteHandler(FileSystemEventHandler):
def on_modified(self, event):
if event.is_directory: return
if event.src_path.endswith(('.html', '.js', '.ts', '.css', '.vue')):
self.build_vite()
def build_vite(self):
subprocess.run(['npm', 'run', 'build'], cwd=self.app_path, check=True, capture_output=True, text=True)
self.trigger_browser_refresh()
def trigger_browser_refresh(self):
refresh_file = self.app_path / 'static' / 'acmefront' / 'refresh.txt'
refresh_file.parent.mkdir(parents=True, exist_ok=True)
refresh_file.write_text(str(time.time()))
class Command(BaseCommand):
def handle(self, *args, **options):
# locate app path, ensure deps, do initial build, then watch templates/src/static
...
- The handler watches three trees inside the app:
templates/
,src/
, andstatic/
. When a relevant file changes, it rebuilds with Vite and writes a new timestamp torefresh.txt
. The page sees the timestamp change and does a full reload.
Result: Fast, reliable full-page reload without running a separate Vite dev server reverse proxy.
Step 5: Quieting Down the Dev Server Logs
To keep the dev server logs from getting cluttered with polling requests for refresh.txt
, you can add a small logging filter in your Django settings and attach it to the console logging:
class SuppressRefreshPollFilter:
_pattern = re.compile(r'^/static/acmefront/refresh\.txt')
def filter(self, record):
if not hasattr(record, 'args'): return True
try:
path = record.args[0].split()[1]
if self._pattern.match(path):
return False
except Exception:
pass
return True
LOGGING = {
'filters': { 'suppress_refresh_poll': { '()': SuppressRefreshPollFilter } },
'handlers': { 'console': { 'class': 'logging.StreamHandler', 'filters': ['suppress_refresh_poll'] } },
'loggers': { 'django.server': { 'handlers': ['console'], 'level': 'INFO', 'propagate': False } },
}
Step 6: Running It All Locally
Here’s how you get everything up and running on your local machine.
In Terminal 1, run the Django dev server:
python manage.py runserver
In Terminal 2, start the Vite watcher that’s integrated with Django:
python manage.py vite_dev --app acmefront
- Navigate to the app’s URL (e.g.,
/applications/demo/acmefront/
depending on rooturls.py
inclusion) and edit files intemplates/
orsrc/
. On save:- Vite rebuilds to
static/acmefront/dist/
- The command writes
static/acmefront/refresh.txt
- The browser polling script sees the change and performs a full-page reload
- Vite rebuilds to
A Few Final Notes
- Lit works beautifully with Vite’s ES module build, especially when you’re targeting modern browsers (
es2020
). - Using predictable filenames like
[name].js
and[name].css
makes it much easier to reference your assets in templates with{% static %}
. - This setup avoids tightly coupling Django to Vite’s HMR runtime and means you don’t need a dev-time proxy. A simple file-polling system is all you need to coordinate rebuilds and page reloads.
- You can still run
vite dev
for HMR when you’re working on components in isolation, but the workflow I’ve described here keeps the browser lifecycle consistent within Django.
And that’s it! A straightforward way to integrate Django, Lit, and Vite with a reliable hot-reload workflow. Happy coding!