
Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag storeFor implementing a cmd+k search modal in your Next.js + Sanity site, you'll want to combine a command palette UI library with Sanity's content querying. Here's my recommended approach:
Use a dedicated command palette library like cmdk (by Paco Coursey) or kbar. Both are excellent React libraries specifically designed for cmd+k interfaces. cmdk is particularly lightweight and flexible.
Install it:
npm install cmdkFor the actual search, you have a few options:
1. Client-side GROQ queries (simplest for getting started):
import {client} from '@/sanity/lib/client'
async function searchContent(query: string) {
const results = await client.fetch(
`*[_type in ["post", "page"] && [title, body] match $query] | order(_score desc) [0...10] {
_id,
_type,
title,
slug
}`,
{query: `${query}*`}
)
return results
}2. Use score() and boost() functions in GROQ for more sophisticated relevance ranking:
*[_type == "post" && title match $query]
| score(
boost(title match $query, 3),
boost(body match $query, 1)
)
| order(_score desc)3. Server Actions (recommended for production): Create a Next.js server action or API route to handle search queries server-side, which keeps your Sanity token secure and reduces client bundle size.
'use client'
import {Command} from 'cmdk'
import {useState, useEffect} from 'react'
import {useRouter} from 'next/navigation'
export function SearchModal() {
const [open, setOpen] = useState(false)
const [search, setSearch] = useState('')
const [results, setResults] = useState([])
const router = useRouter()
// Keyboard shortcut
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
setOpen((open) => !open)
}
}
document.addEventListener('keydown', down)
return () => document.removeEventListener('keydown', down)
}, [])
// Debounced search
useEffect(() => {
if (!search) return
const timer = setTimeout(async () => {
const data = await fetch(`/api/search?q=${search}`).then(r => r.json())
setResults(data)
}, 300)
return () => clearTimeout(timer)
}, [search])
return (
<Command.Dialog open={open} onOpenChange={setOpen}>
<Command.Input
value={search}
onValueChange={setSearch}
placeholder="Search content..."
/>
<Command.List>
{results.map((item) => (
<Command.Item
key={item._id}
onSelect={() => {
router.push(`/${item.slug.current}`)
setOpen(false)
}}
>
{item.title}
</Command.Item>
))}
</Command.List>
</Command.Dialog>
)
}match operator with wildcards for flexible text matchingThe command palette pattern is great for content sites because it provides quick navigation without cluttering your UI. The combination of cmdk + GROQ queries gives you a powerful, flexible search that feels native to modern web apps.
Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.
Content operations
Content backend


The only platform powering content operations
By Industry


Tecovas strengthens their customer connections
Build and Share

Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag store