Modals
When reaching for a modal, first consider if you can communicate this message in another way.
On this page
Overview
Modals are disruptive, confusing, poorly accessible, blocking the user’s interaction, hard to escape, used as a junk drawer, frustrating on small screens, and add to cognitive load. Consider non-modal dialogs on empy screen space, go inline, expand elements or use a new page.
There are lots of alternatives to modals. Most of the time, if you need to confirm an action, material design suggests instead offering an option to undo. This still gives a user an option to reverse an action, but it does not interrupt their flow and increase their cognitive load. Read more on modalzmodalzmodalz.com
Using Modals
When it's appropriate to use a modal, we have several options in HQ to initialize a modal.
Standard Usage
The standard way to use modals is with HTML—an example is shown below. You can read Bootstrap 5's modal docs for all available background options, sizing, javascript usage, and more.
<!-- Button trigger modal -->
<button
class="btn btn-primary"
type="button"
data-bs-toggle="modal"
data-bs-target="#bs-example-modal"
>
Launch demo modal
</button>
<!-- Modal -->
<div
id="bs-example-modal" class="modal fade"
tabindex="-1" aria-labelledby="example-modal-label" aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 id="example-modal-label" class="modal-title">
Modal Title
</h4>
<button
class="btn-close" type="button" aria-label="Close"
data-bs-dismiss="modal"
></button>
</div>
<div class="modal-body">
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi interdum nibh mattis felis
consequat euismod id sed lorem. Etiam cursus magna id turpis tincidunt, eu volutpat tellus
placerat. In porttitor mauris non felis ullamcorper elementum. Duis at neque accumsan massa
sodales vulputate et eget elit.
</p>
</div>
<div class="modal-footer">
<button
class="btn btn-outline-primary" type="button"
data-bs-dismiss="modal"
>
Close
</button>
<button class="btn btn-primary" type="button">
Save Options
</button>
</div>
</div>
</div>
</div>
Modal Knockout Binding
It is also possible to control modals with Knockout using the modal knockout binding.
This binding accepts an observable as an argument. If the value of the observable is
true, then the modal is shown. If the value is false,
then the modal is hidden. This method eliminates the use of the data-bs-toggle="modal" and
data-bs-target="#modalId" attributes on the modal trigger element, as well as the
data-bs-dismiss="modal" attributes.
The most simplified version of this is to have a boolean observable bound to modal that's
toggled true or false with knockout code. However, the example below is a slightly
more complex approach that's more realistic to how we use it in HQ (like with UserRole editing).
<div id="js-ko-demo-modal">
<div class="row">
<!-- ko foreach: items -->
<div class="col-6">
<div class="card ">
<div class="card-body">
<h5 data-bind="text: name"></h5>
<p data-bind="text: description"></p>
<button
class="btn btn-outline-primary"
type="button"
data-bind="click: $root.setItemBeingEdited"
>
<i class="fa fa-edit"></i> Edit <!-- ko text: name --><!-- /ko -->
</button>
</div>
</div>
</div>
<!-- /ko -->
</div>
<div
class="modal fade"
tabindex="-1" aria-labelledby="example-ko-modal-label" aria-hidden="true"
data-bind="modal: itemBeingEdited"
>
<div class="modal-dialog">
<div
class="modal-content"
data-bind="with: itemBeingEdited"
>
<div class="modal-header">
<h4
id="example-ko-modal-label"
class="modal-title"
data-bind="text: modalTitle"
></h4>
<button
class="btn-close"
type="button" aria-label="Close"
data-bind="click: $root.unsetItemBeingEdited"
></button>
</div>
<form data-bind="submit: $root.submitItemChanges">
<div class="modal-body">
<div class="mb-3">
<label for="id-example-item-name" class="form-label">Name</label>
<input
id="id-example-item-name"
class="form-control"
type="text"
data-bind="value: name"
/>
</div>
<div class="mb-3">
<label for="id-example-item-description" class="form-label">Description</label>
<textarea
id="id-example-item-description"
class="form-control"
rows="3"
data-bind="value: description"
></textarea>
</div>
</div>
<div class="modal-footer">
<button
class="btn btn-outline-primary"
type="button"
data-bind="click: $root.unsetItemBeingEdited"
>
Cancel
</button>
<button class="btn btn-primary" type="submit">
Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
</div>
import $ from 'jquery';
import ko from 'knockout';
let Item = function (id, name, description) {
let self = {};
self.id = ko.observable(id);
self.name = ko.observable(name);
self.description = ko.observable(description);
return self;
};
$("#js-ko-demo-modal").koApplyBindings(function () {
let self = {};
self.items = ko.observableArray([
Item(1, "First", "This is the first test item"),
Item(2, "Second", "This is the second test item"),
]);
self.itemBeingEdited = ko.observable(undefined);
self.submitItemChanges = function () {
for (let i = 0; i < self.items().length; i++) {
if (self.items()[i].id() === self.itemBeingEdited().id()) {
self.items()[i].name(self.itemBeingEdited().name());
self.items()[i].description(self.itemBeingEdited().description());
}
}
self.unsetItemBeingEdited();
};
self.unsetItemBeingEdited = function () {
self.itemBeingEdited(undefined);
};
self.setItemBeingEdited = function (item) {
let tempItem = Item(item.id(), item.name(), item.description());
tempItem.modalTitle = "Editing Item '" + item.name() + "'";
self.itemBeingEdited(tempItem);
};
return self;
});
OpenModal Knockout Binding
This method quickly opens a modal using the openModal knockout binding, which takes a string value
of the id of the knockout template supplying the HTML for this modal.
<div class="modal fade"> wrapper element. Additionally, the modal inherits the
knockout model context that was applied to the element that applied openModal binding.
<div id="js-ko-demo-open-modal">
<button
class="btn btn-primary"
type="button"
data-bind="openModal: 'js-ko-template-open-modal-demo'"
>
Open Modal
</button>
</div>
<script id="js-ko-template-open-modal-demo" type="text/html">
<div class="modal-dialog" aria-labelledby="example-open-modal-label" aria-hidden="true">
<div class="modal-content">
<div class="modal-header">
<h4
id="example-open-modal-label"
class="modal-title"
data-bind="text: modalTitle"
></h4>
<button
class="btn-close"
type="button" aria-label="Close"
data-bs-dismiss="modal"
></button>
</div>
<div class="modal-body">
<p data-bind="text: modalText"></p>
</div>
<div class="modal-footer">
<button
class="btn btn-outline-primary"
type="button"
data-bs-dismiss="modal"
>
Close
</button>
<button type="button" class="btn btn-primary">
Okaaaaay
</button>
</div>
</div>
</div>
</script>
import ko from 'knockout';
$("#js-ko-demo-open-modal").koApplyBindings(function () {
return {
modalTitle: ko.observable("OpenModal Modal Example"),
modalText: ko.observable("The modal obtains its knockout context from the context of the triggering element"),
};
});
OpenRemoteModal Knockout Binding
Another less frequently-used way of triggering a modal is to use the openRemoteModal Knockout Binding.
This binding takes a URL to a view in HQ that renders the modal-dialog content of the modal, similar
to the knockout template above.
<div id="js-ko-demo-open-remote-modal">
<button type="button"
class="btn btn-primary"
data-bind="openRemoteModal: remoteUrl">
Open Modal
</button>
</div>
import $ from 'jquery';
import ko from 'knockout';
import initialPageData from 'hqwebapp/js/initial_page_data';
$(function () {
$("#js-ko-demo-open-remote-modal").koApplyBindings(function () {
return {
remoteUrl: ko.observable(initialPageData.reverse("styleguide_data_remote_modal") + "?testParam=Okaaaaay"),
};
});
});
In the example above, the URL passed to openRemoteModal was registered
on the page using the following template tag:
{% registerurl "styleguide_data_remote_modal" %}
The view for styleguide_data_remote_modal rendered the HTML below. Note that the secret_message
template variable was populated byt the GET parameter testParam specified
at the end of the remoteUrl value as seen in the code above.
<div class="modal-dialog" aria-labelledby="example-open-remote-modal" aria-hidden="true">
<div class="modal-content">
<div class="modal-header">
<h4 id="example-open-remote-modal" class="modal-title">
Remote Modal Example
</h4>
<button
class="btn-close"
type="button" aria-label="Close"
data-bs-dismiss="modal"
></button>
</div>
<div class="modal-body">
<p>
This modal's context is defined by the context of the view that rendered the modal.
See <code>styleguide.views.bootstrap5_data.remote_modal_demo</code> for the view that
rendered this modal.
</p>
</div>
<div class="modal-footer">
<button
class="btn btn-outline-primary" type="button"
data-bs-dismiss="modal"
>
Close
</button>
<button class="btn btn-primary" type="button">
{{ secret_message }}
</button>
</div>
</div>
</div>