diff --git a/examples/README.md b/examples/README.md
index 5343a7b..f1cf9fc 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -10,3 +10,4 @@ Examples with `_codec` show how to use a custom codec. Examples with `_import_ho
- `custom_components` - Shows how you can use custom components
- `props` - Shows some advanced props usage
- `custom_elements` - Shows how you can use [custom HTML elements](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements)
+- 'django_htmx_pyjsx' - How to use pyjsx togther with Htmx and Django
\ No newline at end of file
diff --git a/examples/django_htmx_pyjsx_todo/README.md b/examples/django_htmx_pyjsx_todo/README.md
new file mode 100644
index 0000000..884d258
--- /dev/null
+++ b/examples/django_htmx_pyjsx_todo/README.md
@@ -0,0 +1,33 @@
+# Todo Webapp
+
+A simple todo webapp built with Django, HTMX, and PyJSX.
+
+## Features
+
+- Create new todos
+- Display todos dynamically using HTMX
+
+## Installation
+0. Install uv
+ ```
+ sudo snap install astro-uv
+ ```
+
+1. Install dependencies using uv:
+ ```
+ uv sync
+ ```
+
+2. Run migrations:
+ ```
+ uv run python manage.py migrate
+ ```
+
+3. Start the development server:
+ ```
+ uv run python manage.py runserver
+ ```
+
+## Usage
+
+Visit `http://localhost:8000` to access the todo app.
\ No newline at end of file
diff --git a/examples/django_htmx_pyjsx_todo/manage.py b/examples/django_htmx_pyjsx_todo/manage.py
new file mode 100644
index 0000000..a515d96
--- /dev/null
+++ b/examples/django_htmx_pyjsx_todo/manage.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+import os
+import sys
+
+
+def main():
+ """Run administrative tasks."""
+ # Set up PyJSX before anything else
+ import pyjsx.auto_setup
+
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "todo_project.settings")
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError as exc:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ ) from exc
+ execute_from_command_line(sys.argv)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/django_htmx_pyjsx_todo/pyproject.toml b/examples/django_htmx_pyjsx_todo/pyproject.toml
new file mode 100644
index 0000000..d8955e7
--- /dev/null
+++ b/examples/django_htmx_pyjsx_todo/pyproject.toml
@@ -0,0 +1,21 @@
+[project]
+name = "todo-app"
+version = "0.1.0"
+description = "A simple todo webapp with Django, HTMX, and PyJSX"
+authors = [{ name = "Your Name", email = "you@example.com" }]
+requires-python = "~=3.12"
+readme = "README.md"
+dependencies = [
+ "django~=5.0",
+ "python_jsx ; python_version >= '3.12' and python_version <= '3.14'",
+ "django-htmx>=1.17.0,<2",
+]
+
+[tool.uv]
+
+[tool.uv.sources]
+python_jsx = { git = "https://github.com/tomasr8/pyjsx.git" }
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
diff --git a/examples/django_htmx_pyjsx_todo/templates/base.html b/examples/django_htmx_pyjsx_todo/templates/base.html
new file mode 100644
index 0000000..a0b156f
--- /dev/null
+++ b/examples/django_htmx_pyjsx_todo/templates/base.html
@@ -0,0 +1,19 @@
+{% load static %}
+
+
+
+
+
+ Todo App
+
+
+
+
+{% include "header.html" %}
+
+{% block content %}
+{% endblock %}
+
+{% include "footer.html" %}
+
+
\ No newline at end of file
diff --git a/examples/django_htmx_pyjsx_todo/templates/footer.html b/examples/django_htmx_pyjsx_todo/templates/footer.html
new file mode 100644
index 0000000..2d6f928
--- /dev/null
+++ b/examples/django_htmx_pyjsx_todo/templates/footer.html
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/examples/django_htmx_pyjsx_todo/templates/header.html b/examples/django_htmx_pyjsx_todo/templates/header.html
new file mode 100644
index 0000000..9ed81cd
--- /dev/null
+++ b/examples/django_htmx_pyjsx_todo/templates/header.html
@@ -0,0 +1,2 @@
+
\ No newline at end of file
diff --git a/examples/django_htmx_pyjsx_todo/templates/todo_app/todo_list.html b/examples/django_htmx_pyjsx_todo/templates/todo_app/todo_list.html
new file mode 100644
index 0000000..8dcd03b
--- /dev/null
+++ b/examples/django_htmx_pyjsx_todo/templates/todo_app/todo_list.html
@@ -0,0 +1,25 @@
+{% extends "base.html" %}
+{% load static %}
+{% block content %}
+
+
Todo List
+
+
+
+
+
+
+ {{ todos_list|safe }}
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/examples/django_htmx_pyjsx_todo/todo_app/__init__.py b/examples/django_htmx_pyjsx_todo/todo_app/__init__.py
new file mode 100644
index 0000000..8eda998
--- /dev/null
+++ b/examples/django_htmx_pyjsx_todo/todo_app/__init__.py
@@ -0,0 +1 @@
+import pyjsx.auto_setup
diff --git a/examples/django_htmx_pyjsx_todo/todo_app/admin.py b/examples/django_htmx_pyjsx_todo/todo_app/admin.py
new file mode 100644
index 0000000..a09bb65
--- /dev/null
+++ b/examples/django_htmx_pyjsx_todo/todo_app/admin.py
@@ -0,0 +1,7 @@
+from django.contrib import admin
+from .models import Todo
+
+@admin.register(Todo)
+class TodoAdmin(admin.ModelAdmin):
+ list_display = ("title", "created_at")
+ search_fields = ("title",)
diff --git a/examples/django_htmx_pyjsx_todo/todo_app/apps.py b/examples/django_htmx_pyjsx_todo/todo_app/apps.py
new file mode 100644
index 0000000..1aac3ba
--- /dev/null
+++ b/examples/django_htmx_pyjsx_todo/todo_app/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+class TodoAppConfig(AppConfig):
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "todo_app"
diff --git a/examples/django_htmx_pyjsx_todo/todo_app/migrations/0001_initial.py b/examples/django_htmx_pyjsx_todo/todo_app/migrations/0001_initial.py
new file mode 100644
index 0000000..7406971
--- /dev/null
+++ b/examples/django_htmx_pyjsx_todo/todo_app/migrations/0001_initial.py
@@ -0,0 +1,23 @@
+# Generated by Django 5.2.6 on 2025-09-15 15:12
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="Todo",
+ fields=[
+ ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
+ ("title", models.CharField(max_length=200)),
+ ("created_at", models.DateTimeField(auto_now_add=True)),
+ ("completed", models.BooleanField(default=False)),
+ ],
+ ),
+ ]
diff --git a/examples/django_htmx_pyjsx_todo/todo_app/migrations/__init__.py b/examples/django_htmx_pyjsx_todo/todo_app/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/examples/django_htmx_pyjsx_todo/todo_app/models.py b/examples/django_htmx_pyjsx_todo/todo_app/models.py
new file mode 100644
index 0000000..9e352ef
--- /dev/null
+++ b/examples/django_htmx_pyjsx_todo/todo_app/models.py
@@ -0,0 +1,9 @@
+from django.db import models
+
+class Todo(models.Model):
+ title = models.CharField(max_length=200)
+ created_at = models.DateTimeField(auto_now_add=True)
+ completed = models.BooleanField(default=False)
+
+ def __str__(self):
+ return self.title
diff --git a/examples/django_htmx_pyjsx_todo/todo_app/todo_components.px b/examples/django_htmx_pyjsx_todo/todo_app/todo_components.px
new file mode 100644
index 0000000..6c3e6e1
--- /dev/null
+++ b/examples/django_htmx_pyjsx_todo/todo_app/todo_components.px
@@ -0,0 +1,25 @@
+# coding: jsx
+from pyjsx import jsx, JSX
+from django.db.models import QuerySet
+from todo_app.models import Todo
+
+def Todo(title: str, completed: bool = False, **rest) -> JSX:
+ """A component that represents a single todo item"""
+ status_class = "line-through text-gray-500" if completed else "text-gray-800"
+ return (
+
+ {title}
+
+ )
+
+def TodoList(todos: QuerySet[Todo], **rest) -> JSX:
+ """A component that renders a list of todos"""
+ if not todos:
+ return No todos yet.
+
+ return (
+
+ {[ for todo in todos]}
+
+ )
+
diff --git a/examples/django_htmx_pyjsx_todo/todo_app/urls.py b/examples/django_htmx_pyjsx_todo/todo_app/urls.py
new file mode 100644
index 0000000..71b0465
--- /dev/null
+++ b/examples/django_htmx_pyjsx_todo/todo_app/urls.py
@@ -0,0 +1,7 @@
+from django.urls import path
+from . import views
+
+urlpatterns = [
+ path("", views.todo_list, name="todo_list"),
+ path("create/", views.create_todo, name="create_todo"),
+]
diff --git a/examples/django_htmx_pyjsx_todo/todo_app/views.py b/examples/django_htmx_pyjsx_todo/todo_app/views.py
new file mode 100644
index 0000000..d3be2eb
--- /dev/null
+++ b/examples/django_htmx_pyjsx_todo/todo_app/views.py
@@ -0,0 +1,22 @@
+from django.shortcuts import render
+from django.http import HttpResponse
+from .models import Todo
+from django.views.decorators.http import require_http_methods
+from django.views.decorators.csrf import csrf_protect
+from .todo_components import TodoList
+
+# Http Views
+@csrf_protect
+def todo_list(request):
+ todos = Todo.objects.all().order_by("-created_at")
+ return render(request, "todo_app/todo_list.html", {"todos_list": TodoList(todos)})
+
+@require_http_methods(["POST"])
+@csrf_protect
+def create_todo(request):
+ title = request.POST.get("title")
+ if title:
+ Todo.objects.create(title=title)
+ # For HTMX, we'll return the updated list
+ todos = Todo.objects.all().order_by("-created_at")
+ return HttpResponse(TodoList(todos))
diff --git a/examples/django_htmx_pyjsx_todo/todo_project/__init__.py b/examples/django_htmx_pyjsx_todo/todo_project/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/examples/django_htmx_pyjsx_todo/todo_project/asgi.py b/examples/django_htmx_pyjsx_todo/todo_project/asgi.py
new file mode 100644
index 0000000..c30474f
--- /dev/null
+++ b/examples/django_htmx_pyjsx_todo/todo_project/asgi.py
@@ -0,0 +1,16 @@
+"""
+ASGI config for todo_project project.
+
+It exposes the ASGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
+"""
+
+import os
+
+from django.core.asgi import get_asgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "todo_project.settings")
+
+application = get_asgi_application()
diff --git a/examples/django_htmx_pyjsx_todo/todo_project/settings.py b/examples/django_htmx_pyjsx_todo/todo_project/settings.py
new file mode 100644
index 0000000..556cd6b
--- /dev/null
+++ b/examples/django_htmx_pyjsx_todo/todo_project/settings.py
@@ -0,0 +1,129 @@
+"""
+Django settings for todo_project project.
+
+Generated by 'django-admin startproject' using Django 5.0.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.0/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/5.0/ref/settings/
+"""
+
+from pathlib import Path
+
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+BASE_DIR = Path(__file__).resolve().parent.parent
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = "django-insecure-!z6$#8^2+!_5!+!_5!+!_5!+!_5!+!_5!+!_5!+!_5!+!_5!+!_5"
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS: list[str] = []
+
+
+# Application definition
+
+INSTALLED_APPS = [
+ "django.contrib.admin",
+ "django.contrib.auth",
+ "django.contrib.contenttypes",
+ "django.contrib.sessions",
+ "django.contrib.messages",
+ "django.contrib.staticfiles",
+ "todo_app",
+ "django_htmx",
+]
+
+MIDDLEWARE = [
+ "django.middleware.security.SecurityMiddleware",
+ "django.contrib.sessions.middleware.SessionMiddleware",
+ "django.middleware.common.CommonMiddleware",
+ "django.middleware.csrf.CsrfViewMiddleware",
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
+ "django.contrib.messages.middleware.MessageMiddleware",
+ "django.middleware.clickjacking.XFrameOptionsMiddleware",
+ "django_htmx.middleware.HtmxMiddleware",
+]
+
+ROOT_URLCONF = "todo_project.urls"
+
+TEMPLATES = [
+ {
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
+ "DIRS": [BASE_DIR / "templates"],
+ "APP_DIRS": True,
+ "OPTIONS": {
+ "context_processors": [
+ "django.template.context_processors.debug",
+ "django.template.context_processors.request",
+ "django.contrib.auth.context_processors.auth",
+ "django.contrib.messages.context_processors.messages",
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = "todo_project.wsgi.application"
+
+
+# Database
+# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
+
+DATABASES = {
+ "default": {
+ "ENGINE": "django.db.backends.sqlite3",
+ "NAME": BASE_DIR / "db.sqlite3",
+ }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
+ },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/5.0/topics/i18n/
+
+LANGUAGE_CODE = "en-us"
+
+TIME_ZONE = "UTC"
+
+USE_I18N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/5.0/howto/static-files/
+
+STATIC_URL = "static/"
+STATICFILES_DIRS = [
+ BASE_DIR / "static",
+]
+
+# Default primary key field type
+# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
+
+DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
diff --git a/examples/django_htmx_pyjsx_todo/todo_project/urls.py b/examples/django_htmx_pyjsx_todo/todo_project/urls.py
new file mode 100644
index 0000000..492d161
--- /dev/null
+++ b/examples/django_htmx_pyjsx_todo/todo_project/urls.py
@@ -0,0 +1,23 @@
+"""
+URL configuration for todo_project project.
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/5.0/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: path('', views.home, name='home')
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
+Including another URLconf
+ 1. Import the include() function: from django.urls import include, path
+ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
+"""
+from django.contrib import admin
+from django.urls import path, include
+
+urlpatterns = [
+ path("admin/", admin.site.urls),
+ path("", include("todo_app.urls")),
+]
diff --git a/examples/django_htmx_pyjsx_todo/todo_project/wsgi.py b/examples/django_htmx_pyjsx_todo/todo_project/wsgi.py
new file mode 100644
index 0000000..cd56c13
--- /dev/null
+++ b/examples/django_htmx_pyjsx_todo/todo_project/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for todo_project project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "todo_project.settings")
+
+application = get_wsgi_application()
diff --git a/examples/django_htmx_pyjsx_todo/uv.lock b/examples/django_htmx_pyjsx_todo/uv.lock
new file mode 100644
index 0000000..ec181d5
--- /dev/null
+++ b/examples/django_htmx_pyjsx_todo/uv.lock
@@ -0,0 +1,79 @@
+version = 1
+revision = 3
+requires-python = ">=3.12, <4"
+
+[[package]]
+name = "asgiref"
+version = "3.9.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/90/61/0aa957eec22ff70b830b22ff91f825e70e1ef732c06666a805730f28b36b/asgiref-3.9.1.tar.gz", hash = "sha256:a5ab6582236218e5ef1648f242fd9f10626cfd4de8dc377db215d5d5098e3142", size = 36870, upload-time = "2025-07-08T09:07:43.344Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7c/3c/0464dcada90d5da0e71018c04a140ad6349558afb30b3051b4264cc5b965/asgiref-3.9.1-py3-none-any.whl", hash = "sha256:f3bba7092a48005b5f5bacd747d36ee4a5a61f4a269a6df590b43144355ebd2c", size = 23790, upload-time = "2025-07-08T09:07:41.548Z" },
+]
+
+[[package]]
+name = "django"
+version = "5.2.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "asgiref" },
+ { name = "sqlparse" },
+ { name = "tzdata", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/4c/8c/2a21594337250a171d45dda926caa96309d5136becd1f48017247f9cdea0/django-5.2.6.tar.gz", hash = "sha256:da5e00372763193d73cecbf71084a3848458cecf4cee36b9a1e8d318d114a87b", size = 10858861, upload-time = "2025-09-03T13:04:03.23Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f5/af/6593f6d21404e842007b40fdeb81e73c20b6649b82d020bb0801b270174c/django-5.2.6-py3-none-any.whl", hash = "sha256:60549579b1174a304b77e24a93d8d9fafe6b6c03ac16311f3e25918ea5a20058", size = 8303111, upload-time = "2025-09-03T13:03:47.808Z" },
+]
+
+[[package]]
+name = "django-htmx"
+version = "1.24.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "asgiref" },
+ { name = "django" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/75/c3/f9bac5ea1fcd46d422b336cc9fb94e5ca012db6ee5a43c270d4922e8efd3/django_htmx-1.24.1.tar.gz", hash = "sha256:2750c05ff673e4c4a86f0d5e79aaff3c1bdd4b1c73089a4e7a3ad4aa4a008ee7", size = 64764, upload-time = "2025-09-11T20:59:49.868Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/56/fa/0864fdc191816335c50feb5348387434ab170a90c271e8fc4e045d08caca/django_htmx-1.24.1-py3-none-any.whl", hash = "sha256:8fc6feba50f77cc45cebd269c94a30a5c0063dbbdb7af8963062e4d5f7a0c9e4", size = 61852, upload-time = "2025-09-11T20:59:48.312Z" },
+]
+
+[[package]]
+name = "python-jsx"
+version = "0.2.0"
+source = { git = "https://github.com/tomasr8/pyjsx.git#3addc69280113767da4637a1806da43a39cc4971" }
+
+[[package]]
+name = "sqlparse"
+version = "0.5.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e5/40/edede8dd6977b0d3da179a342c198ed100dd2aba4be081861ee5911e4da4/sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", size = 84999, upload-time = "2024-12-10T12:05:30.728Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415, upload-time = "2024-12-10T12:05:27.824Z" },
+]
+
+[[package]]
+name = "todo-app"
+version = "0.1.0"
+source = { editable = "." }
+dependencies = [
+ { name = "django" },
+ { name = "django-htmx" },
+ { name = "python-jsx", marker = "python_full_version < '3.15'" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "django", specifier = "~=5.0" },
+ { name = "django-htmx", specifier = ">=1.17.0,<2" },
+ { name = "python-jsx", marker = "python_full_version >= '3.12' and python_full_version < '3.15'", git = "https://github.com/tomasr8/pyjsx.git" },
+]
+
+[[package]]
+name = "tzdata"
+version = "2025.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
+]