cotton
for
Docs syntax
<c> tags: Snippets will be shown in Cotton's HTML-like tag syntax.
Native: Snippets will be shown in native Django template syntax.

HTMX Integration

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>

Returning Components from Views

For HTMX partial responses, use render_component() to render components directly from your views:

  • Create a Cotton component in cotton/component_name.html
  • Use it in templates: <c-component-name />
  • Return it from views: render_component(request, "component-name", context)
  • HTMX swaps it into the DOM seamlessly
No need for template partials or other libraries - Cotton components work directly as HTMX partials.

1. Inline Editing & Delete

Common CRUD patterns: edit tasks in-place and delete with confirmation. Try clicking "Edit" or "Delete":

demo

Implement user authentication

Add login and registration functionality

Design homepage layout

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('')

2. Form Validation

Validate fields inline on blur without a full page reload:

demo
<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:

demo
Alice Johnson
Bob Smith
Carol White
<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,
        )
    )

4. Search with Debounce

Live search results with debouncing (delay:500ms) to avoid excessive requests. Try searching:

demo

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,
        )
    )

Key Takeaways

  • Use {{ attrs }} to pass HTMX attributes to styled components
  • Use render_component() to return Cotton components as HTMX partial responses
  • No need for template partials or special configuration
  • Components maintain their reusability while serving dynamic content
  • Combine with Alpine.js for enhanced interactivity