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>