Modals Component¶
Version: 0.6.0
Last Updated: January 3, 2026
Location: src/templates/partials/modal.html
Modals are dialog boxes that capture user attention and require interaction before returning to the main content. They're used for confirmations, alerts, forms, and complex workflows.
Overview¶
Modals in Arctyk support:
- Confirmation dialogs (delete, archive, bulk actions)
- Alert messages (success, error, warning)
- Form modals (inline editing, quick create)
- Custom content modals
- Keyboard accessibility (ESC to close)
- Backdrop dismissal
- Multiple concurrent modals with stacking
- Size options (small, default, large)
Basic Usage¶
Confirmation Modal¶
<!-- Trigger Button -->
<button
class="btn btn-danger"
data-bs-toggle="modal"
data-bs-target="#confirmDeleteModal"
>
Delete
</button>
<!-- Modal -->
<div class="modal fade" id="confirmDeleteModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirm Delete</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete this ticket?</p>
<p class="text-muted">This action cannot be undone.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Cancel
</button>
<button type="button" class="btn btn-danger" id="confirmDelete">
Delete
</button>
</div>
</div>
</div>
</div>
Alert Modal¶
<div class="modal fade" id="successModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-success text-white">
<h5 class="modal-title">Success</h5>
<button
type="button"
class="btn-close btn-close-white"
data-bs-dismiss="modal"
></button>
</div>
<div class="modal-body">
<p>{{ message }}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success" data-bs-dismiss="modal">
Close
</button>
</div>
</div>
</div>
</div>
Modal Sizes¶
Sizes¶
<!-- Small Modal -->
<div class="modal fade" id="smallModal" tabindex="-1">
<div class="modal-dialog modal-sm">
<!-- Content -->
</div>
</div>
<!-- Large Modal -->
<div class="modal fade" id="largeModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<!-- Content -->
</div>
</div>
<!-- Extra Large Modal -->
<div class="modal fade" id="xlModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<!-- Content -->
</div>
</div>
Form Modal¶
Example: Inline Field Editor¶
<button
class="btn btn-link"
data-bs-toggle="modal"
data-bs-target="#editModal"
data-field="priority"
>
Edit Priority
</button>
<div class="modal fade" id="editModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Edit Field</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
></button>
</div>
<form id="editForm">
<div class="modal-body">
<div class="form-group">
<label class="form-label">Priority</label>
<select class="form-control" name="priority">
<option value="low">Low</option>
<option value="high">High</option>
<option value="critical">Critical</option>
</select>
</div>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
data-bs-dismiss="modal"
>
Cancel
</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>
</div>
</div>
</div>
JavaScript Handling¶
Programmatic Modal Control¶
// Get modal instance
const modal = new bootstrap.Modal(document.getElementById("editModal"));
// Show modal
modal.show();
// Hide modal
modal.hide();
// Toggle modal
modal.toggle();
Event Handling¶
const modalElement = document.getElementById("confirmDeleteModal");
const modal = new bootstrap.Modal(modalElement);
// Before modal shows
modalElement.addEventListener("show.bs.modal", function (event) {
const button = event.relatedTarget;
const ticketId = button.dataset.ticketId;
document.getElementById("confirmDelete").dataset.ticketId = ticketId;
});
// Modal shown
modalElement.addEventListener("shown.bs.modal", function () {
document.getElementById("confirmDelete").focus();
});
// Confirm deletion
document.getElementById("confirmDelete").addEventListener("click", function () {
const ticketId = this.dataset.ticketId;
fetch(`/tickets/${ticketId}/`, {
method: "DELETE",
headers: {
"X-CSRFToken": getCookie("csrftoken"),
},
}).then((response) => {
if (response.ok) {
modal.hide();
location.reload();
}
});
});
AJAX Form Submission¶
const editForm = document.getElementById("editForm");
editForm.addEventListener("submit", function (e) {
e.preventDefault();
const formData = new FormData(editForm);
const ticketId = this.dataset.ticketId;
fetch(`/tickets/${ticketId}/edit-field/`, {
method: "POST",
headers: {
"X-CSRFToken": getCookie("csrftoken"),
},
body: formData,
})
.then((response) => response.json())
.then((data) => {
if (data.success) {
modal.hide();
showAlert("success", "Field updated successfully");
location.reload();
} else {
showAlert("danger", data.error);
}
});
});
Accessibility¶
ARIA Attributes¶
<div
class="modal fade"
id="deleteModal"
tabindex="-1"
aria-labelledby="deleteModalLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel">Delete Ticket</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div
class="modal-body"
role="region"
aria-label="Delete confirmation message"
>
<p>Are you sure you want to delete this item?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Cancel
</button>
<button
type="button"
class="btn btn-danger"
aria-label="Delete item permanently"
>
Delete
</button>
</div>
</div>
</div>
</div>
Keyboard Navigation¶
<!-- Focus visible on important buttons -->
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
data-bs-dismiss="modal"
autofocus
>
Cancel
</button>
<button type="button" class="btn btn-danger" id="confirmButton">
Delete
</button>
</div>
<script>
// ESC key support
document.addEventListener("keydown", function (e) {
if (e.key === "Escape") {
modal.hide();
}
});
// Tab trap for modal
const focusableElements = document.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
lastElement.addEventListener("keydown", function (e) {
if (e.key === "Tab") {
e.preventDefault();
firstElement.focus();
}
});
</script>
Testing¶
Unit Tests¶
from django.test import TestCase, Client
from tickets.models import Ticket
class TicketDeleteTest(TestCase):
def setUp(self):
self.client = Client()
self.ticket = Ticket.objects.create(title='Test')
def test_delete_confirmation_modal_renders(self):
response = self.client.get(f'/tickets/{self.ticket.id}/')
self.assertContains(response, 'confirmDeleteModal')
self.assertContains(response, 'Are you sure')
JavaScript Tests¶
describe("Modal", () => {
let modal;
let element;
beforeEach(() => {
element = document.createElement("div");
element.innerHTML = `
<div class="modal fade" id="testModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5>Test Modal</h5>
</div>
</div>
</div>
</div>
`;
document.body.appendChild(element);
modal = new bootstrap.Modal(element.querySelector(".modal"));
});
it("should show modal", (done) => {
modal.show();
setTimeout(() => {
expect(element.querySelector(".modal").classList.contains("show")).toBe(
true
);
done();
}, 100);
});
it("should hide modal", (done) => {
modal.show();
modal.hide();
setTimeout(() => {
expect(element.querySelector(".modal").classList.contains("show")).toBe(
false
);
done();
}, 100);
});
});
Best Practices¶
✅ Use meaningful titles that describe the action
✅ Provide clear confirmation messages
✅ Make destructive actions obvious (red buttons)
✅ Always include a cancel option
✅ Set focus to primary button when modal opens
✅ Use appropriate modal sizes for content
✅ Support keyboard navigation (ESC to close)
❌ Don't use modals for large forms
❌ Don't remove the backdrop
❌ Don't nest modals
❌ Don't use modals for navigation
Common Patterns¶
Confirmation Workflow¶
// 1. User clicks delete button
// 2. Modal appears with confirmation
// 3. User clicks confirm button
// 4. AJAX request sent
// 5. Modal closes on success
// 6. Page refreshes or updates
const deleteButton = document.getElementById("deleteButton");
deleteButton.addEventListener("click", function () {
const ticketId = this.dataset.id;
showConfirmModal("Delete Ticket?", "This cannot be undone.", function () {
deleteTicket(ticketId);
});
});
Multi-Step Modal¶
// Step 1: Select item
// Step 2: Configure settings
// Step 3: Review and confirm
// Step 4: Execute action
class MultiStepModal {
constructor(element) {
this.element = element;
this.step = 1;
}
nextStep() {
this.step++;
this.render();
}
previousStep() {
this.step--;
this.render();
}
render() {
// Hide all steps
this.element.querySelectorAll("[data-step]").forEach((el) => {
el.style.display = "none";
});
// Show current step
this.element.querySelector(`[data-step="${this.step}"]`).style.display =
"block";
}
}