← Controllers

search

Search modal with Pagefind integration and keyboard shortcuts.

Press Cmd+K (Mac) or Ctrl+K (Windows/Linux) right now. The search modal that opens is powered by this controller.

Usage

<div data-controller="search">
  <!-- Trigger Button -->
  <button data-action="click->search#open">
    Search
  </button>

  <!-- Modal -->
  <div data-search-target="modal"
       data-action="click->search#closeOnBackdrop keydown.escape@window->search#close"
       class="hidden fixed inset-0 bg-gray-900/50 z-50">
    <div class="container mx-auto px-4 py-16 max-w-2xl">
      <div class="bg-white rounded-lg shadow-xl">
        <div class="flex justify-between items-center p-4 border-b">
          <h2>Search</h2>
          <button data-action="click->search#close">Close</button>
        </div>
        <div data-search-target="container" class="p-4"></div>
      </div>
    </div>
  </div>
</div>

Targets

Target Purpose
modal The modal overlay
container Container for Pagefind UI

Keyboard Shortcuts

Shortcut Action
Cmd+K / Ctrl+K Open search
Escape Close search

Pagefind Setup

Add Pagefind to your build script:

{
  "scripts": {
    "build": "NODE_ENV=production eleventy && npx pagefind --site dist"
  }
}

Include Pagefind assets in your scripts partial:

<link href="/pagefind/pagefind-ui.css" rel="stylesheet">
<script src="/pagefind/pagefind-ui.js"></script>

Source

import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  static targets = ["modal", "container"];

  connect() {
    this.isOpen = false;
    this.boundHandleKeydown = this.handleKeydown.bind(this);
    document.addEventListener("keydown", this.boundHandleKeydown);
    this.initializePagefind();
  }

  disconnect() {
    document.removeEventListener("keydown", this.boundHandleKeydown);
  }

  initializePagefind() {
    if (typeof PagefindUI !== "undefined" && this.hasContainerTarget) {
      new PagefindUI({
        element: this.containerTarget,
        showSubResults: true,
        showImages: false,
      });
    }
  }

  open() {
    this.isOpen = true;
    this.modalTarget.classList.remove("hidden");
    setTimeout(() => {
      const input = this.modalTarget.querySelector("input");
      if (input) input.focus();
    }, 100);
  }

  close() {
    this.isOpen = false;
    this.modalTarget.classList.add("hidden");
  }

  handleKeydown(event) {
    if ((event.ctrlKey || event.metaKey) && event.key === "k") {
      event.preventDefault();
      this.open();
    }
  }

  closeOnBackdrop(event) {
    if (event.target === this.modalTarget) this.close();
  }
}