Modal

Modal dialog overlay component.

template 1 required 8 optional 4 slots 2 a11y rules

Preview

Usage

views.py
python
from djust import LiveView
from djust.decorators import event_handler


class MyView(LiveView):
    template_name = "my_template.html"

    def mount(self, request, **kwargs):
        self.id = 'gallery-modal-md'
        self.title = 'Modal (md)'
        self.size = 'md'
        self.is_open = False

    @event_handler()
    def toggle_modal(self, **kwargs):
        self.is_open = not self.is_open
my_template.html
django
{% load theme_components %}
<button type="button" dj-click="toggle_modal" style="font: inherit; padding: 0.375rem 0.75rem; border-radius: 0.375rem; border: 1px solid hsl(var(--border)); background: hsl(var(--card)); color: hsl(var(--card-foreground)); cursor: pointer; margin-bottom: 0.75rem;">Open modal</button>
{% theme_modal id=id title=title size=size is_open=is_open %}

Events this component sends to the view: toggle_modal.

Props

NameTypeRequiredDefaultDescription
idstrrequired——
titleOptional[str]——
sizestrmd—
css_prefixstr——
attrsdict——
slot_headerstr——
slot_bodystr——
slot_footerstr——
slot_closestr——

Accessibility

RequirementElementAttributeValue
Modal must have role=dialogdivroledialog
Modal must have aria-modal=truedivaria-modaltrue

Slots

slot_header slot_body slot_footer slot_close
Source & styles
FilePathNotes
templatedjust_theming/components/modal.htmlCopy it to the same path in your project, or per theme under djust_theming/themes/<theme>/components/.
cssdjust_theming/components.cssDefines 7 of this component's classes at line 457, 468, 485, 493, 511, 515, 522. Override those rules, or the custom properties they read, in a stylesheet loaded after it.
cssdjust_theming/scaffold.cssDefines 6 of this component's classes at line 679, 692, 711, 719, 728, 747. Override those rules, or the custom properties they read, in a stylesheet loaded after it.
cssdjust_components/components.cssDefines 4 of this component's classes at line 56, 57, 60, 63. Override those rules, or the custom properties they read, in a stylesheet loaded after it.
CSS variables
--border --card --card-foreground --foreground --muted --primary --radius --ring --surface-1 --surface-2 --warning --warning-text
Template source — modal.html
django
{% comment %}
Server-driven modal. `is_open` comes from the host LiveView's `Modal`
descriptor, and the close control dispatches `toggle_modal`.

Visibility is an inline `display`, and it was forced to be: `.modal-backdrop` is
`display: flex` in BOTH `theming/css/components.css` and `scaffold.css`, and an
author rule beats the UA's `[hidden] { display: none }`, so `hidden` here would
have rendered the dialog permanently visible. `components.css` now carries a
`[hidden]` guard with `!important` (#3290), so the attribute would work for a
page that links it. The inline style stays anyway: it is what the retired JS
did, and it keeps working for a page that loads `scaffold.css` without
`components.css`.

Escape-to-close now works through the framework rather than `components.js`:
`51-keyboard-nav.js:_closeModal` dispatches the dialog's explicit close control,
the `[dj-click]` element marked `data-dj-close` (or `.dj-modal__close`), which
this button carries. A `slot_close` replacement must carry `data-dj-close` and a
`dj-click` too, or Escape no longer reaches the server. Focus trapping and
`aria-modal` come from the same module.

`data-theme-modal` / `data-theme-modal-close` below are the client fallback,
and they are load-bearing for plain pages: `components.js` finds the backdrop
with `[data-theme-modal="<id>"]`, so removing them (as a first pass at this did)
leaves the theming gallery's triggers pointing at nothing. LiveView pages never
use them — the guard in `components.js` stands down when a djust mount root is
present.

One behaviour is deliberately dropped: click-the-backdrop-to-close. It cannot
be expressed with a plain `dj-click` on the backdrop, because the event bubbles
— clicking anywhere inside the dialog would close it. There is no `dj-self`
modifier in the client to guard it. The close button and Escape both still
work. Restoring it needs either that modifier or a sibling scrim element
positioned behind the dialog, which is a markup change worth making on purpose
rather than as a side effect of this one.
{% endcomment %}
<div class="{{ css_prefix }}modal-backdrop"
     data-theme-modal="{{ id }}"
     {% if is_open %}data-open="true"{% else %}style="display:none;"{% endif %}>
    <div class="{{ css_prefix }}modal {{ css_prefix }}modal-{{ size }} {% if attrs.class %}{{ attrs.class }}{% endif %}"
         role="dialog"
         aria-modal="true"
         {% if title %}aria-labelledby="{{ id }}-title"{% endif %}
         {% if attrs.id %}id="{{ attrs.id }}"{% endif %}>
        {% if slot_close %}
        {{ slot_close|safe }}
        {% else %}
        <button class="{{ css_prefix }}modal-close" aria-label="Close"
                data-theme-modal-close data-dj-close
                dj-click="toggle_modal" data-value="{{ id }}"
                {% if component_id %}data-component-id="{{ component_id }}"{% endif %}>
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                <line x1="18" y1="6" x2="6" y2="18"></line>
                <line x1="6" y1="6" x2="18" y2="18"></line>
            </svg>
        </button>
        {% endif %}
        {% if slot_header %}
        <div class="{{ css_prefix }}modal-header">
            {{ slot_header|safe }}
        </div>
        {% elif title %}
        <div class="{{ css_prefix }}modal-header">
            <h2 class="{{ css_prefix }}modal-title" id="{{ id }}-title">{{ title }}</h2>
        </div>
        {% endif %}
        <div class="{{ css_prefix }}modal-body">
            {% if slot_body %}{{ slot_body|safe }}{% elif body %}{{ body }}{% endif %}
        </div>
        {% if slot_footer %}
        <div class="{{ css_prefix }}modal-footer">
            {{ slot_footer|safe }}
        </div>
        {% endif %}
    </div>
</div>