Skip to main content
This guide shows you how to build custom DocSearch integrations using the core packages. Whether you’re building for a new framework, a custom documentation platform, or need advanced customization, this guide covers the fundamentals.

Understanding DocSearch Packages

DocSearch is built as a modular system with several packages:

@docsearch/js

Vanilla JavaScript wrapper using Preact

@docsearch/react

React components and hooks

@docsearch/core

Framework-agnostic core logic

@docsearch/css

Styles and CSS variables

Architecture Overview

DocSearch is built on top of Algolia’s Autocomplete library:

Building a Framework Integration

Approach 1: Wrap the JavaScript Package

The simplest approach is to wrap @docsearch/js:
1

Install dependencies

npm install @docsearch/js @docsearch/css
2

Create a wrapper component

Example for Vue 3:
DocSearch.vue
<script setup>
import { onMounted, onUnmounted, ref } from 'vue'
import docsearch from '@docsearch/js'
import '@docsearch/css'

const props = defineProps({
  appId: { type: String, required: true },
  apiKey: { type: String, required: true },
  indexName: { type: String, required: true },
  placeholder: { type: String, default: 'Search docs' },
  searchParameters: { type: Object, default: () => ({}) },
})

const instance = ref(null)

onMounted(() => {
  instance.value = docsearch({
    container: '#docsearch',
    appId: props.appId,
    apiKey: props.apiKey,
    indexName: props.indexName,
    placeholder: props.placeholder,
    searchParameters: props.searchParameters,
  })
})

onUnmounted(() => {
  instance.value?.destroy()
})

// Expose methods
defineExpose({
  open: () => instance.value?.open(),
  close: () => instance.value?.close(),
})
</script>

<template>
  <div id="docsearch"></div>
</template>
3

Use the component

App.vue
<script setup>
import DocSearch from './components/DocSearch.vue'
</script>

<template>
  <div>
    <header>
      <h1>My Docs</h1>
      <DocSearch
        app-id="YOUR_APP_ID"
        api-key="YOUR_SEARCH_API_KEY"
        index-name="YOUR_INDEX_NAME"
      />
    </header>
  </div>
</template>

Approach 2: Use React Components Directly

Many frameworks support React components. Example for Solid.js:
DocSearch.tsx
import { createSignal, onMount, onCleanup } from 'solid-js'
import { DocSearch as DocSearchReact } from '@docsearch/react'
import '@docsearch/css'

export function DocSearch(props) {
  return (
    <DocSearchReact
      appId={props.appId}
      apiKey={props.apiKey}
      indexName={props.indexName}
    />
  )
}

Approach 3: Build from Core

For complete control, use @docsearch/core directly:
custom-docsearch.ts
import { useDocSearch, DocSearchProvider } from '@docsearch/core'
import type { DocSearchProps } from '@docsearch/core'

// Create your own UI using the core hooks
export function createDocSearch(props: DocSearchProps) {
  // Use core logic
  const {
    query,
    collections,
    isOpen,
    recentSearches,
    favoriteSearches,
    openModal,
    closeModal,
    // ... other state and methods
  } = useDocSearch()

  // Build your custom UI with this data
  return {
    query,
    collections,
    isOpen,
    open: openModal,
    close: closeModal,
    // Expose what you need
  }
}

Custom Search Client

You can customize the Algolia search client:
custom-client.ts
import docsearch from '@docsearch/js'
import { liteClient as algoliasearch } from 'algoliasearch/lite'

const instance = docsearch({
  container: '#docsearch',
  appId: 'YOUR_APP_ID',
  apiKey: 'YOUR_SEARCH_API_KEY',
  indexName: 'YOUR_INDEX_NAME',
  transformSearchClient: (searchClient) => {
    // Add custom Algolia agent
    searchClient.addAlgoliaAgent('my-integration', '1.0.0')

    // Intercept search requests
    const originalSearch = searchClient.search
    searchClient.search = async (requests) => {
      console.log('Searching:', requests)
      const results = await originalSearch(requests)
      console.log('Results:', results)
      return results
    }

    return searchClient
  },
})

Custom Result Transformations

Transform search results before rendering:
import docsearch from '@docsearch/js'

const instance = docsearch({
  container: '#docsearch',
  appId: 'YOUR_APP_ID',
  apiKey: 'YOUR_SEARCH_API_KEY',
  indexName: 'YOUR_INDEX_NAME',
  transformItems: (items) => {
    return items.map((item) => {
      // Transform URLs
      if (item.url.startsWith('/')) {
        item.url = `https://docs.example.com${item.url}`
      }

      // Add custom metadata
      item.metadata = {
        lastUpdated: item._highlightResult?.lastModified?.value,
      }

      // Filter out certain results
      if (item.hierarchy.lvl0 === 'Internal') {
        return null
      }

      return item
    }).filter(Boolean)
  },
})

Custom Navigation

Control how DocSearch navigates:
import docsearch from '@docsearch/js'

const instance = docsearch({
  container: '#docsearch',
  appId: 'YOUR_APP_ID',
  apiKey: 'YOUR_SEARCH_API_KEY',
  indexName: 'YOUR_INDEX_NAME',
  navigator: {
    navigate({ itemUrl }) {
      // Custom navigation logic
      if (itemUrl.startsWith('http')) {
        // External link - open in new tab
        window.open(itemUrl, '_blank')
      } else {
        // Internal link - use your router
        yourRouter.push(itemUrl)
      }
    },
    navigateNewTab({ itemUrl }) {
      window.open(itemUrl, '_blank')
    },
    navigateNewWindow({ itemUrl }) {
      window.open(itemUrl, '_blank', 'noopener,noreferrer')
    },
  },
})

Custom Hit Component

Customize how individual results are rendered:
import docsearch from '@docsearch/js'

const instance = docsearch({
  container: '#docsearch',
  appId: 'YOUR_APP_ID',
  apiKey: 'YOUR_SEARCH_API_KEY',
  indexName: 'YOUR_INDEX_NAME',
  hitComponent: ({ hit, children }, { html }) => {
    // Get the result type from hierarchy
    const type = hit.type || 'content'
    const icon = {
      content: '📄',
      lvl1: '📂',
      lvl2: '📋',
    }[type] || '📄'

    return html`
      <a href="${hit.url}" class="custom-hit" data-type="${type}">
        <span class="hit-icon">${icon}</span>
        <div class="hit-content">
          <div class="hit-title">
            ${children}
          </div>
          ${hit.hierarchy.lvl0 && html`
            <div class="hit-path">
              ${hit.hierarchy.lvl0}
            </div>
          `}
        </div>
      </a>
    `
  },
})
Add a custom footer to the results:
import docsearch from '@docsearch/js'

const instance = docsearch({
  container: '#docsearch',
  appId: 'YOUR_APP_ID',
  apiKey: 'YOUR_SEARCH_API_KEY',
  indexName: 'YOUR_INDEX_NAME',
  resultsFooterComponent: ({ state }, { html }) => {
    const nbHits = state.context?.nbHits || 0
    const query = state.query

    if (!query) return null

    return html`
      <div class="custom-footer">
        <div class="results-count">
          Found ${nbHits} results for "${query}"
        </div>
        <div class="footer-actions">
          <a href="/search?q=${encodeURIComponent(query)}" class="view-all">
            View all results
          </a>
          <a href="/feedback?q=${encodeURIComponent(query)}" class="feedback">
            Results not relevant?
          </a>
        </div>
      </div>
    `
  },
})

TypeScript Integration

Create type-safe wrappers:
docsearch-wrapper.ts
import docsearch from '@docsearch/js'
import type { DocSearchProps, DocSearchInstance } from '@docsearch/js'
import '@docsearch/css'

export interface CustomDocSearchProps extends Omit<DocSearchProps, 'container'> {
  // Add custom props
  onSearch?: (query: string) => void
  onSelect?: (item: any) => void
  analytics?: {
    trackSearch: (query: string) => void
    trackClick: (item: any) => void
  }
}

export function createDocSearch(
  container: string | HTMLElement,
  props: CustomDocSearchProps
): DocSearchInstance {
  const { onSearch, onSelect, analytics, ...docSearchProps } = props

  // Create custom transformSearchClient
  const originalTransform = props.transformSearchClient
  const transformSearchClient = (searchClient: any) => {
    let client = searchClient
    
    // Apply original transform first
    if (originalTransform) {
      client = originalTransform(client)
    }

    // Add analytics
    if (analytics) {
      const originalSearch = client.search
      client.search = async (requests: any[]) => {
        const query = requests[0]?.params?.query
        if (query) {
          analytics.trackSearch(query)
        }
        return originalSearch(requests)
      }
    }

    return client
  }

  // Create instance with enhanced props
  const instance = docsearch({
    container,
    ...docSearchProps,
    transformSearchClient,
    transformItems: (items) => {
      // Call original transform if provided
      const transformed = props.transformItems?.(items) ?? items
      
      // Add click tracking
      if (analytics) {
        return transformed.map((item) => ({
          ...item,
          onClick: () => {
            analytics.trackClick(item)
            onSelect?.(item)
          },
        }))
      }
      
      return transformed
    },
  })

  return instance
}
Usage:
const instance = createDocSearch('#docsearch', {
  appId: 'YOUR_APP_ID',
  apiKey: 'YOUR_SEARCH_API_KEY',
  indexName: 'YOUR_INDEX_NAME',
  analytics: {
    trackSearch: (query) => {
      console.log('Search:', query)
    },
    trackClick: (item) => {
      console.log('Click:', item)
    },
  },
})

Server-Side Rendering

Handle SSR in your custom integration:
docsearch-ssr.ts
import { useEffect, useRef } from 'react'
import type { DocSearchProps } from '@docsearch/js'

export function DocSearchSSR(props: Omit<DocSearchProps, 'container'>) {
  const containerRef = useRef<HTMLDivElement>(null)
  const instanceRef = useRef<any>(null)

  useEffect(() => {
    // Only run on client
    if (typeof window === 'undefined') return

    // Dynamically import to avoid SSR issues
    import('@docsearch/js').then(({ default: docsearch }) => {
      if (containerRef.current && !instanceRef.current) {
        instanceRef.current = docsearch({
          container: containerRef.current,
          ...props,
        })
      }
    })

    // Cleanup
    return () => {
      instanceRef.current?.destroy()
      instanceRef.current = null
    }
  }, [props])

  return <div ref={containerRef} />
}

Analytics Integration

Track search usage:
import docsearch from '@docsearch/js'
import { track } from './analytics'

const instance = docsearch({
  container: '#docsearch',
  appId: 'YOUR_APP_ID',
  apiKey: 'YOUR_SEARCH_API_KEY',
  indexName: 'YOUR_INDEX_NAME',
  onOpen: () => {
    track('search_opened')
  },
  onClose: () => {
    track('search_closed')
  },
  transformSearchClient: (searchClient) => {
    const originalSearch = searchClient.search
    
    searchClient.search = async (requests) => {
      const query = requests[0]?.params?.query
      if (query) {
        track('search_query', { query, timestamp: Date.now() })
      }
      
      const results = await originalSearch(requests)
      const hits = results.results[0]?.hits?.length || 0
      
      track('search_results', { query, hits })
      
      return results
    }
    
    return searchClient
  },
  navigator: {
    navigate({ itemUrl, item }) {
      track('search_result_clicked', {
        url: itemUrl,
        title: item.hierarchy?.lvl1 || item.hierarchy?.lvl0,
      })
      window.location.href = itemUrl
    },
  },
})

A/B Testing

Implement A/B testing for search:
import docsearch from '@docsearch/js'

// Determine variant
const variant = Math.random() < 0.5 ? 'A' : 'B'

const instance = docsearch({
  container: '#docsearch',
  appId: 'YOUR_APP_ID',
  apiKey: 'YOUR_SEARCH_API_KEY',
  indexName: 'YOUR_INDEX_NAME',
  searchParameters: {
    // Variant A: default behavior
    // Variant B: boost recent content
    ...(variant === 'B' && {
      optionalFilters: ['recentContent:true<score=2>'],
    }),
  },
  transformSearchClient: (searchClient) => {
    // Add variant to user agent
    searchClient.addAlgoliaAgent('ab-test', variant)
    return searchClient
  },
})

// Track which variant performed better
window.addEventListener('beforeunload', () => {
  analytics.track('ab_variant', { variant, searches: searchCount })
})

Testing Your Integration

Write tests for your custom integration:
docsearch.test.ts
import { describe, it, expect, vi } from 'vitest'
import { createDocSearch } from './docsearch-wrapper'

describe('DocSearch Custom Integration', () => {
  it('should initialize with correct props', () => {
    const container = document.createElement('div')
    container.id = 'docsearch'
    document.body.appendChild(container)

    const instance = createDocSearch('#docsearch', {
      appId: 'test-app-id',
      apiKey: 'test-api-key',
      indexName: 'test-index',
    })

    expect(instance).toBeDefined()
    expect(instance.isReady).toBe(true)
  })

  it('should call analytics on search', async () => {
    const trackSearch = vi.fn()
    
    const instance = createDocSearch('#docsearch', {
      appId: 'test-app-id',
      apiKey: 'test-api-key',
      indexName: 'test-index',
      analytics: {
        trackSearch,
        trackClick: vi.fn(),
      },
    })

    // Simulate search
    instance.open()
    // ... trigger search

    expect(trackSearch).toHaveBeenCalled()
  })
})

Best Practices

Always clean up DocSearch instances when components unmount:
onUnmounted(() => {
  instance.value?.destroy()
})
  • Lazy load DocSearch only when needed
  • Use the onTouchStart, onFocus, and onMouseOver callbacks to preload
  • Implement debouncing for search queries
Use TypeScript for better developer experience:
import type { DocSearchProps, DocSearchHit } from '@docsearch/js'
Handle errors gracefully:
transformSearchClient: (searchClient) => {
  const originalSearch = searchClient.search
  searchClient.search = async (requests) => {
    try {
      return await originalSearch(requests)
    } catch (error) {
      console.error('Search failed:', error)
      // Show user-friendly error
      return { results: [] }
    }
  }
  return searchClient
}
Ensure your custom integration maintains accessibility:
  • Keep keyboard navigation working
  • Preserve ARIA attributes
  • Test with screen readers

Examples by Framework

Vue 3

See the Vue 3 example above for wrapping @docsearch/js

Svelte

Similar to Vue, use onMount and onDestroy lifecycle hooks

Angular

Create a directive or component that initializes DocSearch in ngAfterViewInit

Next.js

Use dynamic imports to avoid SSR issues

Next Steps

API Reference

Complete API documentation

Styling

Customize appearance

Core Package

Use @docsearch/core directly

Contributing

Contribute your integration

Build docs developers (and LLMs) love