Customization
Learn how to customize the Typesense Search Plugin to match your needs and design.
Component Customization
Custom Styling
Override the default styles with CSS:
/* Custom search input */
.headless-search-input input {
border: 2px solid #667eea;
border-radius: 12px;
padding: 16px 20px;
font-size: 18px;
background: #f8fafc;
}
.headless-search-input input:focus {
border-color: #5a67d8;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
/* Custom results */
.headless-search-input .search-result {
padding: 20px;
border-radius: 8px;
margin-bottom: 8px;
background: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.headless-search-input .search-result:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
Custom Result Rendering
Create completely custom result displays:
// Single collection with custom rendering
<HeadlessSearchInput
baseUrl="http://localhost:3000"
collection="posts"
renderResult={(hit, index) => (
<div key={index} className="custom-result">
<h3>{hit.document.title}</h3>
<p>{hit.document.content}</p>
<span className="collection-badge">{hit.collection}</span>
</div>
)}
/>
// Multi-collection with custom rendering
<HeadlessSearchInput
baseUrl="http://localhost:3000"
collections={['posts', 'products']}
renderResult={(hit, index) => (
<div key={index} className="custom-result">
<div className="result-header">
<span className="collection-badge">
{hit.icon} {hit.displayName || hit.collection}
</span>
<span className="score">{Math.round((hit.text_match || 0) * 100)}%</span>
</div>
<h3 className="result-title">
{hit.document.title || hit.document.filename || hit.document.name}
</h3>
<p className="result-excerpt">
{hit.document.shortDescription || hit.document.content?.substring(0, 150)}...
</p>
{hit.highlight && (
<div className="highlights">
{Object.entries(hit.highlight).map(([field, highlight]) => (
<div key={field} className="highlight-field">
<strong>{field}:</strong>
<span
dangerouslySetInnerHTML={{
__html: highlight,
}}
/>
</div>
))}
</div>
)}
<div className="result-meta">
<span className="date">
{hit.document.updatedAt ? new Date(hit.document.updatedAt).toLocaleDateString() : 'N/A'}
</span>
<span className="id">ID: {hit.document.id}</span>
</div>
</div>
)}
/>
Custom Loading and Error States
<HeadlessSearchInput
baseUrl="http://localhost:3000"
collection="posts"
renderLoading={() => (
<div className="loading-state">
<div className="spinner"></div>
<span>Searching across all collections...</span>
</div>
)}
renderNoResults={(query) => (
<div className="no-results">
<div className="no-results-icon">🔍</div>
<h3>No results for "{query}"</h3>
<p>Try different keywords or check your spelling.</p>
</div>
)}
onError={(error) => (
<div className="error-state">
<div className="error-icon">⚠️</div>
<h3>Search Error</h3>
<p>Something went wrong. Please try again.</p>
</div>
)}
/>
HeadlessSearchInput Customization
For complete control over rendering, use the HeadlessSearchInput:
<HeadlessSearchInput
baseUrl="http://localhost:3000"
collection="posts"
renderInput={(props) => (
<div className="search-input-wrapper">
<input {...props} />
<span className="search-icon">🔍</span>
</div>
)}
renderResult={(result, index) => (
<div
key={result.id}
className="custom-result-item"
data-result-item
onClick={() => console.log('Clicked:', result)}
>
<h3>{result.title}</h3>
<p>{result.document.content}</p>
</div>
)}
renderResultsHeader={(found, searchTime) => (
<div className="results-header">
Found {found} results in {searchTime}ms
</div>
)}
/>
Search Behavior Customization
Search Parameters
The plugin supports these search parameters:
q
- Search query (required)page
- Page number (1-based, default: 1)per_page
- Results per page (1-250, default: 10)sort_by
- Sort field and direction (optional)
Collection Configuration
Customize search behavior per collection:
collections: {
posts: {
enabled: true,
displayName: 'Blog Posts',
searchFields: ['title', 'content', 'excerpt'],
facetFields: ['category', 'status', 'author'],
icon: '📝',
sortFields: ['createdAt', 'updatedAt', 'title']
}
}
Advanced Search
Use the POST endpoint for custom Typesense parameters:
curl -X POST "http://localhost:3000/api/search/posts" \
-H "Content-Type: application/json" \
-d '{
"q": "typesense",
"query_by": "title,content",
"filter_by": "status:published",
"sort_by": "createdAt:desc",
"highlight_full_fields": "title,content",
"num_typos": 1,
"snippet_threshold": 30
}'
Performance Optimization
Debouncing
<HeadlessSearchInput
baseUrl="http://localhost:3000"
collection="posts"
debounceMs={500} // Increase for slower networks
minQueryLength={3} // Reduce API calls
/>
Result Limiting
<HeadlessSearchInput
baseUrl="http://localhost:3000"
collection="posts"
perPage={5} // Limit results for faster rendering
/>
Caching
The plugin includes built-in caching:
- Cache Key: Based on query, collection, and parameters
- TTL: 5 minutes (300,000ms) by default
- Max Size: 1000 entries by default
- Hit Rate: Available in health check responses
Theme Integration
The plugin includes a comprehensive theme system with 5 pre-built themes and unlimited customization options.
Quick Theme Usage
// Use pre-built themes
<HeadlessSearchInput theme="modern" /> // Clean and professional
<HeadlessSearchInput theme="minimal" /> // Flat design
<HeadlessSearchInput theme="elegant" /> // Sophisticated with gradients
<HeadlessSearchInput theme="dark" /> // Perfect for dark mode
<HeadlessSearchInput theme="colorful" /> // Vibrant and modern
// Custom theme configuration
<HeadlessSearchInput
theme={{
theme: 'modern',
colors: {
inputBorderFocus: '#10b981',
inputBackground: '#f0fdf4',
},
enableAnimations: true,
enableShadows: true,
}}
/>
📖 Complete Theme System Documentation - Learn about all theme options, custom configurations, performance settings, and advanced features.
Legacy CSS Styling (Deprecated)
Note: The new theme system replaces the need for custom CSS. Use the theme system for better maintainability and consistency.
/* Dark mode styles - DEPRECATED: Use theme="dark" instead */
@media (prefers-color-scheme: dark) {
.headless-search-input input {
background: #1a202c;
color: #e2e8f0;
border-color: #4a5568;
}
.headless-search-input .search-results {
background: #2d3748;
border-color: #4a5568;
}
.headless-search-input .search-result {
color: #e2e8f0;
}
.headless-search-input .search-result:hover {
background: #4a5568;
}
}
Brand Colors
:root {
--search-primary: #667eea;
--search-primary-hover: #5a67d8;
--search-background: #f8fafc;
--search-border: #e2e8f0;
--search-text: #2d3748;
}
.headless-search-input input {
border-color: var(--search-border);
background: var(--search-background);
color: var(--search-text);
}
.headless-search-input input:focus {
border-color: var(--search-primary);
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
Event Handling
Search Events
<HeadlessSearchInput
baseUrl="http://localhost:3000"
collection="posts"
onSearch={(query) => {
console.log('Searching for:', query)
// Track search analytics
}}
onResults={(results) => {
console.log(`Found ${results.found} results in ${results.search_time_ms}ms`)
// Track search completion
}}
onResultClick={(result) => {
console.log('Result clicked:', result.document.title)
// Navigate to result page
}}
onError={(error) => {
console.error('Search error:', error)
// Show user-friendly error message
}}
/>
Custom Analytics
const trackSearchEvent = (event: string, data: any) => {
// Send to your analytics service
analytics.track(event, {
...data,
timestamp: new Date().toISOString(),
userId: getCurrentUserId(),
})
}
;<HeadlessSearchInput
baseUrl="http://localhost:3000"
collection="posts"
onSearch={(query) => {
trackSearchEvent('search_initiated', { query })
}}
onResultClick={(result) => {
trackSearchEvent('search_result_clicked', {
query: result.query,
resultId: result.document.id,
collection: result.collection,
})
}}
onResults={(results) => {
trackSearchEvent('search_completed', {
query: results.request_params.q,
resultCount: results.found,
searchTime: results.search_time_ms,
})
}}
/>
Configuration Customization
Plugin Settings
typesenseSearch({
// Disable plugin
disabled: false,
// Global settings
settings: {
autoSync: true, // Auto-sync documents
batchSize: 100, // Batch size for operations
categorized: false, // Group results by collection
searchEndpoint: '/api/search',
},
// Collection configurations
collections: {
posts: {
enabled: true,
displayName: 'Blog Posts',
searchFields: ['title', 'content'],
facetFields: ['category', 'status'],
icon: '📝',
sortFields: ['createdAt', 'title'],
},
},
})
Typesense Configuration
typesense: {
apiKey: process.env.TYPESENSE_API_KEY,
nodes: [
{
host: process.env.TYPESENSE_HOST,
port: parseInt(process.env.TYPESENSE_PORT),
protocol: 'https'
}
],
connectionTimeoutSeconds: 30,
numRetries: 5,
retryIntervalSeconds: 2
}
Next Steps
- Performance Guide - Optimize for speed
- Troubleshooting - Fix common issues
- API Reference - Explore advanced features