If you use tools like Raycast, Slack, or frequently browse the Vercel or Tailwind documentation, you’ve probably developed a reflex: Cmd+K (or Ctrl+K on Windows).
This keyboard shortcut opens a floating “Command Palette” (or global search bar) that lets you instantly find what you’re looking for without touching your mouse. It has become the absolute standard for modern user experience.
However, implementing true full-text search on an Astro-generated static site (without a database or backend) has long been a headache. Fortunately, Pagefind changed the game.
What is Pagefind?
Pagefind is a search library written in Rust, specifically designed for static websites.
Unlike older methods that forced the visitor’s browser to download a massive index.json file to search for a single word, Pagefind generates a multitude of tiny fragments during your site’s “build” process. The browser only downloads the few kilobytes necessary for its search. It’s magic, free, and requires neither Algolia nor a server.
Step 1: Preparing the Build
Pagefind works by scanning your HTML files after Astro has generated them.
The first step is therefore to modify your package.json file to run Pagefind right after the Astro build.
// package.json
{
"scripts": {
"dev": "astro dev",
"build": "astro build && npx pagefind --site dist",
"preview": "astro preview"
}
}
Note: Replace dist with your output folder if you have changed it.
Step 2: Creating the Modal (Dialog Box)
We are going to use the native HTML <dialog> tag. It is perfect for this: it automatically handles displaying on top of the rest of the site, the grayed-out background (backdrop), and the Esc key to close.
Let’s create a SearchModal.astro component:
---
// src/components/SearchModal.astro
---
<dialog id="search-dialog" class="backdrop:bg-black/50 p-4 rounded-lg shadow-xl w-full max-w-2xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700">
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-semibold">Search</h2>
<button id="close-dialog" class="text-sm px-2 py-1 bg-gray-100 dark:bg-gray-800 rounded">Esc</button>
</div>
<div id="search"></div>
</dialog>
<link href="/pagefind/pagefind-ui.css" rel="stylesheet">
Step 3: The Magic of the Cmd+K Shortcut
This is where we bring our command palette to life. We need to listen for keyboard strokes, display the modal, and initialize Pagefind.
Still in our SearchModal.astro file, let’s add the following script. If you followed my previous tutorials, you’ll notice the use of astro:page-load to guarantee it works with View Transitions (ClientRouter)!
<script>
function initSearch() {
const dialog = document.getElementById('search-dialog') as HTMLDialogElement;
const closeBtn = document.getElementById('close-dialog');
if (!dialog) return;
// 1. Initialize Pagefind UI
// Ensure it's not already initialized to avoid duplicates
if (!document.querySelector('.pagefind-ui__search-input')) {
// @ts-ignore - PagefindUI is globally injected by the script
new window.PagefindUI({ element: "#search", showSubResults: true });
}
// 2. Handle the keyboard shortcut (Cmd+K or Ctrl+K)
window.addEventListener('keydown', (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault(); // Prevents the browser's default behavior
dialog.showModal();
// Auto-focus on the Pagefind search input
setTimeout(() => {
document.querySelector<HTMLInputElement>('.pagefind-ui__search-input')?.focus();
}, 50);
}
});
// 3. Close the modal when clicking the "Esc" button
closeBtn?.addEventListener('click', () => {
dialog.close();
});
// 4. Close the modal if clicking outside of it
dialog.addEventListener('click', (e) => {
const rect = dialog.getBoundingClientRect();
const isInDialog = (rect.top <= e.clientY && e.clientY <= rect.top + rect.height && rect.left <= e.clientX && e.clientX <= rect.left + rect.width);
if (!isInDialog) {
dialog.close();
}
});
}
// Compatible with View Transitions!
document.addEventListener('astro:page-load', () => {
// The Pagefind script is only available after the build
// We add it dynamically
const script = document.createElement('script');
script.src = "/pagefind/pagefind-ui.js";
script.onload = initSearch;
document.head.appendChild(script);
});
</script>
Step 4: Final Integration
All that’s left is to import <SearchModal /> at the root of your main Layout (for example, in BaseLayout.astro) so that the search is available on all your pages.
Run npm run build, then serve the dist folder, and press Cmd+K… Enjoy your new instant search bar!
Quick tip: Don’t hesitate to override Pagefind’s CSS variables (like --pagefind-ui-primary) in your global stylesheet so the search bar perfectly matches your blog’s theme and Dark Mode.