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

Documentation

Disclosure

A native details/summary disclosure for optional content.

All components Component Brickwork 4.3.1

Try it

FAQ answer

Open full-page preview

A native disclosure with authored safe panel content.

Can I use this without JavaScript?
<p>Yes. The browser owns the details interaction.</p>
Use for FAQs, expandable supporting information, and small progressive-disclosure sections.

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/disclosure.html

Django template

{% extends "component_cookbook/base.html" %}{% load i18n %}{% block heading %}{% translate "Disclosure" %}{% endblock %}{% block introduction %}{% translate "A native details control works without JavaScript." %}{% endblock %}{% block cookbook_content %}{% include "brickwork/components/_disclosure.html" with label="What does this demonstrate?" content="A native details element remains interactive without JavaScript." variant="bordered" %}{% 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

{% include "brickwork/components/_disclosure.html" with label=label content=faq_answer %}

your_app/views.py

Python

context = {'faq_answer': '<p>Yes. The browser owns the details interaction.</p>', 'label': 'Can I use this without JavaScript?'}

Guide

Add it to your project

  1. Supply a visible summary and pre-rendered content.
  2. Include the partial.
  3. Use `open` only when initial expanded state is genuinely useful.

View this component's source for the installed release

Options

Public API

Option Type Default Required Description
label str Not set Yes Native summary text. Constraint: Use concise visible text that identifies the control or measurement.
content SafeString Not set Yes Pre-rendered panel content. Constraint: Pass only pre-rendered, trusted SafeString markup; do not mark request data safe.
variant str divided No Visual treatment. Allowed: bordered, divided. Constraint: Use one of the documented variants; use `danger`, not an invented `error` severity.
name str Not set No Shared name creates native single-open accordion behaviour. Constraint: Use the server-side field name expected by the receiving form or query parser.
open bool False No Initial details state. Constraint: No additional package-level validation is documented beyond this option's stated type and its role in the component.
trigger_meta SafeString|template block Not set No Inline trigger-end content. 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 details and summary provide keyboard and assistive-technology semantics without custom ARIA roles.
Responsive
No breakpoint switch.
Without JavaScript
Fully openable and closable with browser behaviour alone.

Need help?

Troubleshooting

Contract check: States: closed (default) and open ([open] on the native <details>), plus

Use the native details state for open and closed content; do not depend on JavaScript to expose the disclosure.

Explore next

Used in examples

Documentation