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'
self.title = 'Modal (md)'
self.size = 'md'
self.is_open = False
@event_handler()
def toggle_modal(self, **kwargs):
self.is_open = not self.is_openmy_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
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| 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 457, 468, 485, 493, 511, 515, 522. 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 679, 692, 711, 719, 728, 747. 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 --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>