Skip to main content
brickwork
GitHub Log in Get started
Display
Theme
Density
Direction
Brand

Documentation

Toggle

A standalone native checkbox styled as a switch.

All components Component Brickwork 4.3.1

Try it

Email setting

Open full-page preview

Enabled setting switch.

Use for an individually posted setting, not a Django form-bound checkbox.

Use it

Working invocation

The live demonstration uses this site's routes. For a portable implementation, download the complete Django and Brickwork cookbook: every file below is shown exactly as it ships in the archive.

Download the consumer cookbook

component_cookbook/views.py

Python

"""Deterministic interaction endpoints: no models, storage, or external calls."""
from __future__ import annotations

from django.contrib import messages
from django.core.paginator import Paginator
from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest
from django.shortcuts import redirect, render
from django.urls import reverse
from django.utils.cache import patch_vary_headers
from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _
from django.views.decorators.http import require_http_methods, require_POST

from .forms import FilterForm, LanguageForm, MessageForm, UploadForm

LANGUAGES = (("python", "Python"), ("django", "Django"), ("htmx", "HTMX"), ("alpine", "Alpine"))
ROWS = tuple({"name": name, "status": status} for name, status in (
    ("Modal response", "ready"), ("Combobox endpoint", "draft"), ("Toast delivery", "ready"),
    ("No-JavaScript floor", "draft"), ("Slide-over focus", "ready"), ("Tooltip focus", "draft"),
    ("Dropdown links", "ready"), ("Native disclosure", "draft"), ("Tag input carrier", "ready"),
    ("Dropzone metadata", "draft"), ("Table selection", "ready"), ("Search request", "draft"),
))


def _htmx(request: HttpRequest) -> bool:
    return request.headers.get("HX-Request") == "true"


def _branch(request: HttpRequest, partial: str, page: str, context: dict, status: int = 200) -> HttpResponse:
    response = render(request, partial if _htmx(request) else page, context, status=status)
    patch_vary_headers(response, ("HX-Request",))
    return response


def _page(request: HttpRequest, template: str, **context) -> HttpResponse:
    return render(request, template, context)


def index(request): return _page(request, "component_cookbook/index.html")
def dropdown(request):
    return _page(request, "component_cookbook/dropdown.html", items=[
        {"label": _("Open cookbook"), "url": reverse("component_cookbook:index"), "icon": "external-link"},
        {"divider": True}, {"label": _("Delete draft"), "url": "#delete", "variant": "danger", "icon": "trash"},
    ])
def tabs(request):
    active = request.GET.get("tab", "overview")
    if active not in {"overview", "api"}:
        return HttpResponseBadRequest("Unknown cookbook tab.")
    return _page(request, "component_cookbook/tabs.html", active=active, tabs=[{"key": "overview", "label": _("Overview")}, {"key": "api", "label": "API"}], overview=mark_safe("<p>Overview content comes from this consumer template.</p>"), api=mark_safe("<p>The API panel uses the same package tab relationship.</p>"))
def tooltip(request): return _page(request, "component_cookbook/tooltip.html", tooltip_id="cookbook-tooltip", text=_("Explains the button"), placement="top")
def disclosure(request): return _page(request, "component_cookbook/disclosure.html")
def tag_input(request): return _page(request, "component_cookbook/tag_input.html")


def modal(request):
    return _branch(request, "component_cookbook/partials/modal.html", "component_cookbook/modal.html", {"form": MessageForm(), "modal_id": "cookbook-message-modal", "close_href": reverse("component_cookbook:index")})


@require_POST
def modal_submit(request):
    form = MessageForm(request.POST)
    context = {"form": form, "modal_id": "cookbook-message-modal", "close_href": reverse("component_cookbook:index")}
    if not form.is_valid():
        return _branch(request, "component_cookbook/partials/modal.html", "component_cookbook/modal.html", context, 422 if _htmx(request) else 200)
    if _htmx(request):
        response = HttpResponse(status=204)
        response["HX-Trigger"] = '{"bw:modal:close": {"id": "cookbook-message-modal"}}'
        return response
    messages.success(request, _("The demo accepted your message without saving it."))
    return redirect("component_cookbook:modal")


def slide_over(request):
    size = request.GET.get("size", "md")
    placement = request.GET.get("placement", "end")
    if size not in {"sm", "md", "lg"} or placement not in {"start", "end"}:
        return HttpResponseBadRequest("Invalid slide-over options.")
    return _branch(request, "component_cookbook/partials/slide_over.html", "component_cookbook/slide_over.html", {"close_href": reverse("component_cookbook:index"), "size": size, "placement": placement})


@require_POST
def toggle(request):
    return _branch(request, "component_cookbook/partials/toggle_outcome.html", "component_cookbook/toggle.html", {"enabled": request.POST.get("notices") == "on"})


@require_POST
def toast(request):
    if not _htmx(request):
        messages.success(request, _("The server delivered this demonstration outcome."))
        response = redirect("component_cookbook:toast-page")
    else:
        response = render(request, "component_cookbook/partials/toast_oob.html")
    patch_vary_headers(response, ("HX-Request",))
    return response


def toast_page(request):
    return _page(request, "component_cookbook/toast_region.html")


def toast_region(request):
    return _page(request, "component_cookbook/toast_region.html")


def combobox(request): return _page(request, "component_cookbook/combobox.html", form=LanguageForm(), options_url=reverse("component_cookbook:combobox-options"))


def combobox_options(request):
    query = request.GET.get("q", "").strip().casefold()[:40]
    return render(request, "component_cookbook/partials/combobox_options.html", {"choices": [choice for choice in LANGUAGES if not query or query in choice[1].casefold()]})


@require_POST
def combobox_submit(request):
    form = LanguageForm(request.POST)
    context = {"form": form, "options_url": reverse("component_cookbook:combobox-options")}
    if form.is_valid():
        context["selected_label"] = dict(LANGUAGES)[form.cleaned_data["language"]]
    return _branch(request, "component_cookbook/partials/combobox_form.html", "component_cookbook/combobox.html", context, 422 if _htmx(request) and not form.is_valid() else 200)


def _rows(query: str, status: str):
    return [row for row in ROWS if (not query or query.casefold() in row["name"].casefold()) and (not status or row["status"] == status)]


def table(request):
    form = FilterForm(request.GET)
    query, status = (form.cleaned_data["q"], form.cleaned_data["status"]) if form.is_valid() else ("", "")
    context = {"filter_form": form, "page_obj": Paginator(_rows(query, status), 4).get_page(request.GET.get("page", 1)), "table_url": reverse("component_cookbook:table"), "form_invalid": not form.is_valid()}
    return _branch(request, "component_cookbook/partials/table_region.html", "component_cookbook/table.html", context)


def search(request):
    query = request.GET.get("q", "").strip()[:40]
    return _page(request, "component_cookbook/search.html", query=query, result_count=len(_rows(query, "")), search_url=reverse("component_cookbook:search"))


@require_http_methods(["GET", "POST"])
def upload(request):
    if request.method == "GET":
        return _page(request, "component_cookbook/upload.html", form=UploadForm())
    form = UploadForm(request.POST, request.FILES)
    context = {"form": form}
    if form.is_valid():
        uploaded = form.cleaned_data["file"]
        context["outcome"] = _("Validated %(name)s (%(size)s bytes). The application did not retain it.") % {"name": uploaded.name, "size": uploaded.size}
        context["form"] = UploadForm()
    return _branch(request, "component_cookbook/partials/upload_form.html", "component_cookbook/upload.html", context, 422 if _htmx(request) and not form.is_valid() else 200)

component_cookbook/urls.py

Python

from django.urls import path

from . import views

app_name = "component_cookbook"

urlpatterns = [
    path("", views.index, name="index"),
    path("dropdown/", views.dropdown, name="dropdown"),
    path("tabs/", views.tabs, name="tabs"),
    path("tooltip/", views.tooltip, name="tooltip"),
    path("disclosure/", views.disclosure, name="disclosure"),
    path("tag-input/", views.tag_input, name="tag-input"),
    path("modal/", views.modal, name="modal"),
    path("modal/submit/", views.modal_submit, name="modal-submit"),
    path("slide-over/", views.slide_over, name="slide-over"),
    path("toggle/", views.toggle, name="toggle"),
    path("toasts/", views.toast_page, name="toast-page"),
    path("toast-region/", views.toast_region, name="toast-region"),
    path("toast/", views.toast, name="toast"),
    path("combobox/", views.combobox, name="combobox"),
    path("combobox/options/", views.combobox_options, name="combobox-options"),
    path("combobox/submit/", views.combobox_submit, name="combobox-submit"),
    path("table/", views.table, name="table"),
    path("search/", views.search, name="search"),
    path("upload/", views.upload, name="upload"),
]

templates/component_cookbook/partials/toggle_outcome.html

Django template

{% load i18n %}<p id="toggle-outcome" aria-live="polite">{% if enabled %}{% translate "Notices are enabled for this request only." %}{% else %}{% translate "Notices are disabled for this request only." %}{% endif %}</p>

templates/component_cookbook/toggle.html

Django template

{% extends "component_cookbook/base.html" %}{% load i18n %}
{% block heading %}{% translate "Toggle outcome" %}{% endblock %}
{% block introduction %}{% translate "This full page is the no-JavaScript result of the toggle form." %}{% endblock %}
{% block cookbook_content %}{% include "component_cookbook/partials/toggle_outcome.html" %}<p><a href="{% url 'component_cookbook:index' %}">{% translate "Return to the cookbook" %}</a></p>{% endblock %}

templates/component_cookbook/index.html

Django template

{% extends "component_cookbook/base.html" %}{% load i18n %}
{% block heading %}{% translate "Interactive component cookbook" %}{% endblock %}
{% block introduction %}{% translate "Each route is a complete consumer-owned example using only Django and django-brickwork 4.2.0." %}{% endblock %}
{% block cookbook_content %}<ul class="bw-stack bw-stack--xs">
{% for name, label in recipes %}{% empty %}
<li><a href="{% url 'component_cookbook:dropdown' %}">Dropdown</a></li><li><a href="{% url 'component_cookbook:tabs' %}">Tabs</a></li><li><a href="{% url 'component_cookbook:tooltip' %}">Tooltip</a></li><li><a href="{% url 'component_cookbook:disclosure' %}">Disclosure</a></li><li><a href="{% url 'component_cookbook:tag-input' %}">Tag input</a></li><li><a href="{% url 'component_cookbook:modal' %}">Modal</a></li><li><a href="{% url 'component_cookbook:slide-over' %}">Slide-over</a></li><li><a href="{% url 'component_cookbook:combobox' %}">Combobox</a></li><li><a href="{% url 'component_cookbook:table' %}">Data table, filter bar and pagination</a></li><li><a href="{% url 'component_cookbook:search' %}">Search</a></li><li><a href="{% url 'component_cookbook:upload' %}">Dropzone</a></li>
{% endfor %}</ul>
<section class="bw-stack bw-stack--xs"><h2>{% translate "Overlay triggers" %}</h2><a class="bw-btn bw-btn--primary" href="{% url 'component_cookbook:modal' %}" hx-get="{% url 'component_cookbook:modal' %}" hx-target="#bw-modal-root" hx-swap="innerHTML">{% translate "Open modal" %}</a><a class="bw-btn bw-btn--secondary" href="{% url 'component_cookbook:slide-over' %}" hx-get="{% url 'component_cookbook:slide-over' %}" hx-target="#bw-slide-over-root" hx-swap="innerHTML">{% translate "Open slide-over" %}</a></section><section class="bw-stack bw-stack--xs"><h2>{% translate "POST demonstrations" %}</h2><form method="post" action="{% url 'component_cookbook:toggle' %}" hx-post="{% url 'component_cookbook:toggle' %}" hx-target="#toggle-outcome" hx-swap="outerHTML">{% csrf_token %}{% load brickwork_components %}{% bw_toggle "Enable notices" id="cookbook-notices" name="notices" %}<button class="bw-btn bw-btn--primary" type="submit">{% translate "Submit toggle" %}</button></form><p id="toggle-outcome" aria-live="polite">{% translate "The value is not saved." %}</p><form method="post" action="{% url 'component_cookbook:toast' %}" hx-post="{% url 'component_cookbook:toast' %}" hx-swap="none">{% csrf_token %}<button class="bw-btn bw-btn--secondary" type="submit">{% translate "Send toast" %}</button></form></section>{% endblock %}

templates/component_cookbook/base.html

Django template

{% extends "brickwork/shell/centred.html" %}
{% load static i18n %}
{% block page_title %}{% translate "Brickwork component cookbook" %}{% endblock %}
{% block head_js %}<script>window.BRICKWORK_JS_URL = "{% static 'brickwork/dist/brickwork.js' %}";</script>{% endblock %}
{% block body_js %}
<script src="{% static 'component_cookbook/vendor/htmx.min.js' %}"></script>
<script type="module">
import Alpine from "{% static 'component_cookbook/vendor/alpine.module.js' %}";
import focus from "{% static 'component_cookbook/vendor/focus.module.js' %}";
window.htmx.config.responseHandling = [{code: "204", swap: false}, {code: "422", swap: true}, {code: "[23]..", swap: true}, {code: "[45]..", swap: false, error: true}];
Alpine.plugin(focus);
const { registerBrickworkComponents } = await import(window.BRICKWORK_JS_URL);
registerBrickworkComponents(Alpine);
window.Alpine = Alpine;
Alpine.start();
</script>
{% endblock %}
{% block content %}
<header class="bw-stack bw-stack--sm"><p><a href="{% url 'component_cookbook:index' %}">{% translate "Component cookbook" %}</a></p><h1>{% block heading %}{% endblock %}</h1><p>{% block introduction %}{% endblock %}</p></header>
{% if messages %}{% for message in messages %}<p class="bw-alert bw-alert--success" role="status">{{ message }}</p>{% endfor %}{% endif %}
{% block cookbook_content %}{% endblock %}
{% endblock %}

templates/your_app/component.html

Django template

{% load brickwork_components %}
{% bw_toggle label=label id="email-notifications" name="email_notifications" checked=checked %}

your_app/views.py

Python

context = {'checked': True, 'label': 'Email notifications'}

Guide

Add it to your project

  1. Load `brickwork_components`.
  2. Call `bw_toggle` with a non-empty label and id.
  3. Put htmx posting attributes on your surrounding consumer element when needed.

View this component's source for the installed release

Options

Public API

Option Type Default Required Description
label string Not set Yes Accessible switch label. Constraint: Use concise visible text that identifies the control or measurement.
id string Not set Yes Input id. Constraint: Use a stable id-safe token of letters, digits, hyphens, or underscores; it must be unique on the page.
name string Not set No Submitted name; defaults to id. Constraint: Use the server-side field name expected by the receiving form or query parser.
value string on No Submitted checked value. Constraint: Use the same type and scale expected by the receiving form or measurement.
checked boolean False No Initial native checkbox state. Constraint: No additional package-level validation is documented beyond this option's stated type and its role in the component.
disabled boolean False No Disabled state. Constraint: No additional package-level validation is documented beyond this option's stated type and its role in the component.

Behaviour

Accessibility and responsive behaviour

Accessibility
Native checkbox with `role=switch` supplies checked semantics and Space/Enter interaction.
Responsive
No breakpoint switch.
Without JavaScript
Entire interaction is native and works without JavaScript.

Need help?

Troubleshooting

Contract check: never {% include %} directly, the tag enforces a non-empty accessible name

Use the toggle tag with a non-empty label. It enforces the accessible name for the switch at render time.

Documentation