Skip to content

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>

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";
  }
}

Resources