Modal
Modal dialog overlay component.
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'
@event_handler()
def toggle_modal(self, value=None, **kwargs):
... # write to self.component; the re-render carries itmy_template.html
django
{% load theme_components %}
{% theme_modal id=id title='Modal (md)' size='md' %}Events this component sends to the view: toggle_modal.
Props
| Name | Type | Required | Default |
|---|---|---|---|
| id | str | required | — |
| title | Optional[str] | — | |
| size | str | md | |
| css_prefix | str | — | |
| attrs | dict | — | |
| slot_header | str | — | |
| slot_body | str | — | |
| slot_footer | str | — | |
| slot_close | str | — |
Accessibility
| Requirement | Element | Attribute | Value |
|---|---|---|---|
| Modal must have role=dialog | div | role | dialog |
| Modal must have aria-modal=true | div | aria-modal | true |
Slots
slot_header slot_body slot_footer slot_close
Source & styles
| File | Path | Notes |
|---|---|---|
| template | djust_theming/components/modal.html | Copy it to the same path in your project, or per theme under djust_theming/themes/<theme>/components/. |
| css | djust_theming/components.css | Defines 7 of this component's classes at line 392, 403, 419, 427, 445, 449, 456. Override those rules, or the custom properties they read, in a stylesheet loaded after it. |
| css | djust_theming/scaffold.css | Defines 6 of this component's classes at line 677, 690, 709, 717, 726, 745. Override those rules, or the custom properties they read, in a stylesheet loaded after it. |
| css | djust_components/components.css | Defines 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
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`, not the `hidden` attribute, and that is
forced: `.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 }` regardless of specificity — so `hidden` here
would render the dialog permanently visible. Emitting the inline style is also
what the retired JS did, so nothing about the cascade changes.
Escape-to-close now works through the framework rather than `components.js`:
`51-keyboard-nav.js:_closeModal` locates a dialog's close control with
`[dj-click]` and dispatches it, which this button carries. 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
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>