Cotton helps you build re-usable HTMX-powered components. Define your styles and markup once, then pass different HTMX attributes via {{ attrs }}:
<c-button hx-post="/follow" hx-swap="outerHTML">Follow</c-button>
<c-button hx-delete="/posts/1" hx-confirm="Delete?">Delete</c-button>
<c-button hx-get="/load-more" hx-target="#results">Load More</c-button>
{% cotton button hx-post="/follow" hx-swap="outerHTML" %}Follow{% endcotton %}
{% cotton button hx-delete="/posts/1" hx-confirm="Delete?" %}Delete{% endcotton %}
{% cotton button hx-get="/load-more" hx-target="#results" %}Load More{% endcotton %}
<button {{ attrs }} class="btn btn-primary">
{{ slot }}
</button>
For HTMX partial responses, use render_component() to render components directly from your views:
cotton/component_name.html<c-component-name />render_component(request, "component-name", context)Common CRUD patterns: edit tasks in-place and delete with confirmation. Try clicking "Edit" or "Delete":
Add login and registration functionality
Create responsive homepage with hero section
{% for task in tasks %}
<c-task :id="task.id" :title="task.title" :description="task.description" />
{% endfor %}
{% for task in tasks %}
{% cotton task :id="task.id" :title="task.title" :description="task.description" %}{% endcotton %}
{% endfor %}
<div id="task-{{ id }}">
<h3>{{ title }}</h3>
<p>{{ description }}</p>
<button
hx-get="/tasks/{{ id }}/edit"
hx-target="#task-{{ id }}"
hx-swap="outerHTML"
>
Edit
</button>
<button
hx-delete="/tasks/{{ id }}/delete"
hx-target="#task-{{ id }}"
hx-swap="delete"
hx-confirm="Delete this task?"
>
Delete
</button>
</div>
<form
hx-post="/tasks/{{ id }}/update"
hx-target="#task-{{ id }}"
hx-swap="outerHTML"
>
{% csrf_token %}
<input type="text" name="title" value="{{ title }}">
<textarea name="description">{{ description }}</textarea>
<button type="submit">Save</button>
<button
type="button"
hx-get="/tasks/{{ id }}"
hx-target="#task-{{ id }}"
hx-swap="outerHTML"
>
Cancel
</button>
</form>
from django.shortcuts import get_object_or_404
from django.http import HttpResponse
from django_cotton import render_component
from .models import Task
def task_detail(request, id):
task = get_object_or_404(Task, id=id)
return HttpResponse(
render_component(request, "task-display",
id=task.id,
title=task.title,
description=task.description,
)
)
def task_edit(request, id):
task = get_object_or_404(Task, id=id)
return HttpResponse(
render_component(request, "task-edit",
id=task.id,
title=task.title,
description=task.description,
)
)
def task_update(request, id):
task = get_object_or_404(Task, id=id)
task.title = request.POST.get('title')
task.description = request.POST.get('description')
task.save()
return HttpResponse(
render_component(request, "task-display",
id=task.id,
title=task.title,
description=task.description,
)
)
def task_delete(request, id):
task = get_object_or_404(Task, id=id)
task.delete()
return HttpResponse('')
Validate fields inline on blur without a full page reload:
<c-email-field
hx-post="/validate-email"
hx-trigger="blur from:find input"
hx-target="this"
hx-swap="outerHTML"
/>
{% cotton email-field hx-post="/validate-email" hx-trigger="blur from:find input" hx-target="this" hx-swap="outerHTML" %}{% endcotton %}
<div {{ attrs }}>
<label for="email">Email</label>
<input type="email" id="email" name="email" value="{{ value }}">
{% if error %}<p>{{ error }}</p>{% endif %}
</div>
from django.http import HttpResponse
from django.core.validators import validate_email
from django.core.exceptions import ValidationError
from django_cotton import render_component
def validate_email_field(request):
value = request.POST.get('email', '')
error = None
if value:
try:
validate_email(value)
except ValidationError:
error = 'Invalid email address'
return HttpResponse(
render_component(request, "email-field", value=value, error=error)
)
Combine Cotton, HTMX, and Alpine.js to load dynamic content into modals. Click "View Details" on any user:
<c-modal />
{% for user in users %}
<div>
<span>{{ user.name }}</span>
<button
hx-get="/users/{{ user.id }}/details"
hx-target="#modal-content"
@click="$dispatch('open-modal')"
>
View Details
</button>
</div>
{% endfor %}
{% cotton modal %}{% endcotton %}
{% for user in users %}
<div>
<span>{{ user.name }}</span>
<button
hx-get="/users/{{ user.id }}/details"
hx-target="#modal-content"
@click="$dispatch('open-modal')"
>
View Details
</button>
</div>
{% endfor %}
<div x-data="{ open: false }"
x-on:open-modal.window="open = true"
x-on:close-modal.window="open = false">
<div x-show="open" @click="open = false"></div>
<div x-show="open">
<div id="modal-content">{{ slot }}</div>
</div>
</div>
<div>
<h2>{{ name }}</h2>
<p>Email: {{ email }}</p>
<button @click="$dispatch('close-modal')">Close</button>
</div>
from django.http import HttpResponse
from django_cotton import render_component
def user_details(request, id):
user = get_object_or_404(User, id=id)
return HttpResponse(
render_component(request, "user-details",
name=user.get_full_name(),
email=user.email,
)
)
Live search results with debouncing (delay:500ms) to avoid excessive requests. Try searching:
Start typing to search...
<input
type="search"
name="q"
placeholder="Search..."
hx-get="/search"
hx-trigger="keyup changed delay:500ms"
hx-target="#search-results"
hx-include="this"
>
<c-search-results />
<input
type="search"
name="q"
placeholder="Search..."
hx-get="/search"
hx-trigger="keyup changed delay:500ms"
hx-target="#search-results"
hx-include="this"
>
{% cotton search-results %}{% endcotton %}
<div id="search-results">
{% if results %}
{% for result in results %}
<div>
<h4>{{ result.title }}</h4>
<p>{{ result.description }}</p>
</div>
{% endfor %}
{% elif query %}
<p>No results for "{{ query }}"</p>
{% else %}
<p>Start typing to search...</p>
{% endif %}
</div>
from django.http import HttpResponse
from django_cotton import render_component
def search(request):
query = request.GET.get('q', '')
results = Article.objects.filter(title__icontains=query)[:10] if query else []
return HttpResponse(
render_component(request, "search-results",
results=results,
query=query,
)
)
{{ attrs }} to pass HTMX attributes to styled componentsrender_component() to return Cotton components as HTMX partial responses