A fast, accessible, and fully customizable search interface component for Vue 3 applications. Provides an elegant modal search experience with keyboard shortcuts, real-time filtering, and complete style customization. Works seamlessly in Single Page Apps or Server-Side Rendered (SSR) environments (e.g. Nuxt 3).
- Features
- Installation
- Quick Start (SPA)
- Nuxt 3 / SSR Usage
- Component Registration Options
- Props
- Events
- Keyboard Shortcuts
- Customization (Styles / Theming)
- Results Data Structure
- Accessibility
- SSR Notes
- Roadmap
- Development
- Contributing
- Changelog
- License
- Keyboard-first UX: Open with
Ctrl+K/Cmd+K, close withEsc - Real-time filtering: Search as you type with instant results
- Modal interface: Clean overlay design that focuses user attention
- Fully customizable: Override colors for body, input, button, and text
- Accessible: Built with semantic HTML and keyboard navigation
- Lightweight: Minimal dependencies, Vue 3 marked as peer dependency
- SSR compatible: Works in Nuxt 3 and other SSR frameworks
- Auto-focus: Input field receives focus automatically when opened
- Click-away close: Modal closes when clicking outside the content area
- Flexible results: Pass any array of searchable items with custom properties
Using npm:
npm install @todovue/tv-searchUsing yarn:
yarn add @todovue/tv-searchUsing pnpm:
pnpm add @todovue/tv-searchGlobal registration (main.js / main.ts):
import { createApp } from 'vue'
import App from './App.vue'
import { TvSearch } from '@todovue/tv-search'
createApp(App)
.use(TvSearch) // enables <TvSearch /> globally
.mount('#app')Alternatively, register as a component:
import { createApp } from 'vue'
import App from './App.vue'
import { TvSearch } from '@todovue/tv-search'
const app = createApp(App)
app.component('TvSearch', TvSearch)
app.mount('#app')Local import inside a component:
<script setup>
import { ref } from 'vue'
import { TvSearch } from '@todovue/tv-search'
const results = ref([
{
id: 1,
title: 'How to use Vue 3',
description: 'Vue 3 is the latest version of Vue.js',
url: 'https://todovue.com/blog/how-to-use-vue-3',
},
{
id: 2,
title: 'How to use Vite',
description: 'Vite is a build tool for modern web development',
url: 'https://todovue.com/blog/how-to-use-vite',
},
{
id: 3,
title: 'How to use Pinia',
description: 'Pinia is a modern store for Vue 3',
url: 'https://todovue.com/blog/how-to-use-pinia',
},
])
function handleSearch(query) {
console.log('Search query:', query)
// Handle search logic here
}
</script>
<template>
<tv-search
placeholder="Search documentation..."
titleButton="Search"
:results="results"
@search="handleSearch"
/>
</template>Create a plugin file: plugins/tv-search.client.ts (client-only is recommended since it uses keyboard events):
import { defineNuxtPlugin } from '#app'
import { TvSearch } from '@todovue/tv-search'
export default defineNuxtPlugin(nuxtApp => {
nuxtApp.vueApp.component('TvSearch', TvSearch)
})Or use the plugin install method:
import { defineNuxtPlugin } from '#app'
import { TvSearch } from '@todovue/tv-search'
export default defineNuxtPlugin(nuxtApp => {
nuxtApp.vueApp.use(TvSearch)
})Then use anywhere in your Nuxt app:
<template>
<tv-search
placeholder="Search site..."
titleButton="Search"
:results="searchResults"
@search="onSearch"
/>
</template>
<script setup>
const searchResults = ref([
// your search results
])
function onSearch(query) {
// handle search
}
</script>Optional direct import (no plugin needed):
<script setup>
import { TvSearch } from '@todovue/tv-search'
</script>| Approach | When to use |
|---|---|
Global via app.use(TvSearch) |
Design system / used across many pages |
Global via app.component('TvSearch', TvSearch) |
Custom component name / multiple search components |
Local named import import TvSearch from '...' |
Single page usage / code splitting |
Nuxt plugin .client.ts |
SSR apps with client-side interactions |
| Prop | Type | Default | Description | Required |
|---|---|---|---|---|
| placeholder | String | "" |
Placeholder text for the search input field | true |
| titleButton | String | "" |
Text displayed on the search button | true |
| results | Array | [] |
Array of searchable items (see Results Data Structure) | true |
| customStyles | Object | {} |
Custom color scheme for theming (see Customization) | false |
Customize the appearance by passing a customStyles object with any of these properties:
| Property | Type | Default | Description |
|---|---|---|---|
| bgBody | String | "#0E131F" |
Background color of the modal overlay (with 0.9 opacity) |
| bgInput | String | "#B9C4DF" |
Background color of the search input area |
| bgButton | String | "#EF233C" |
Background color of the search button |
| colorButton | String | "#F4FAFF" |
Text color of the search button |
| Event | Payload Type | Description |
|---|---|---|
| search | String | Emitted when search is triggered (Enter key or button click). Returns the trimmed search query. |
Example:
<tv-search
placeholder="Search..."
titleButton="Go"
:results="items"
@search="handleSearch"
/>
<script setup>
function handleSearch(query) {
console.log('User searched for:', query)
// Perform API call, route navigation, etc.
}
</script>| Shortcut | Action |
|---|---|
Ctrl + K / Cmd + K |
Open the search modal |
Escape |
Close the search modal |
Enter |
Execute search with current input |
| Click outside modal | Close the search modal |
You can override the default color scheme by passing a customStyles object:
<script setup>
import { ref } from 'vue'
import { TvSearch } from '@todovue/tv-search'
const customStyles = ref({
bgBody: "#1e1d23",
bgInput: "#8673a1",
bgButton: "#80286e",
colorButton: "#d7c9c9",
})
const results = ref([
// your results
])
</script>
<template>
<tv-search
placeholder="Type to search..."
titleButton="Search"
:results="results"
:customStyles="customStyles"
/>
</template>Dark Theme:
const darkTheme = {
bgBody: "#0E131F",
bgInput: "#1F2937",
bgButton: "#3B82F6",
colorButton: "#FFFFFF",
}Light Theme:
const lightTheme = {
bgBody: "#F9FAFB",
bgInput: "#FFFFFF",
bgButton: "#6366F1",
colorButton: "#FFFFFF",
}Brand Theme:
const brandTheme = {
bgBody: "#0A4539",
bgInput: "#284780",
bgButton: "#80286E",
colorButton: "#D5B7B7",
}The results prop expects an array of objects with the following structure:
interface SearchResult {
id: number | string; // Unique identifier (required for :key)
title: string; // Displayed in search results (required)
description?: string; // Additional info (optional, not currently displayed)
url?: string; // Navigation target (optional, not currently used in component)
[key: string]: any; // Any additional custom properties
}Example:
const results = [
{
id: 1,
title: 'Getting Started with Vue 3',
description: 'Learn the basics of Vue 3 composition API',
url: '/docs/vue3-intro',
category: 'Tutorial',
},
{
id: 2,
title: 'Understanding Reactivity',
description: 'Deep dive into Vue reactivity system',
url: '/docs/reactivity',
category: 'Advanced',
},
]Note: The component currently filters results based on the title property matching the user input (case-insensitive). You can handle the @search event to implement custom search logic or navigation.
- Keyboard navigation: Full support for
Ctrl+K/Cmd+Kto open,Escto close, andEnterto search - Focus management: Input automatically receives focus when modal opens and is selected for immediate typing
- Semantic HTML: Uses proper
<button>,<input>, and modal structure - Click-away: Modal closes when clicking the overlay, providing intuitive UX
Recommendations:
- Provide clear, descriptive
placeholdertext - Use meaningful
titleButtontext (e.g., "Search", "Find", "Go") - Ensure sufficient color contrast when using
customStyles - Consider adding
aria-labelattributes for screen reader support in future versions
- Safe for SSR: No direct DOM access (
window/document) during module initialization - Event listeners: Keyboard event listeners are registered in
onMountedand cleaned up inonBeforeUnmount - Client-side only: Keyboard shortcuts require browser environment; use
.client.tsplugin in Nuxt - Icons: SVG icons are loaded via Vite's
import.meta.glob, which works in both SPA and SSR builds - No manual CSS import required: Styles are automatically injected via
vite-plugin-css-injected-by-js
| Feature | Status |
|---|---|
Support for result url navigation |
Planned |
Display description in results |
Planned |
| Customizable search icon | Planned |
| Multiple keyboard shortcut options | Considering |
| Result categorization / grouping | Considering |
| Highlight matching text in results | Considering |
| Recent searches history | Considering |
| Loading state indicator | Considering |
| Pagination for large result sets | Considering |
| Fuzzy search / advanced filtering | Considering |
| Theming via CSS variables | Considering |
| TypeScript type definitions improvement | Planned |
| ARIA attributes enhancement | Planned |
Clone the repository and install dependencies:
git clone https://github.com/TODOvue/tv-search.git
cd tv-search
yarn installRun development server with demo playground:
yarn devBuild the library:
yarn buildBuild demo site:
yarn build:demoThe demo is served from Vite using index.html + src/demo examples.
Contributions are welcome! Please read our Contributing Guidelines and Code of Conduct before submitting PRs.
How to contribute:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
See CHANGELOG.md for release history and version changes.
MIT © TODOvue
Crafted for the TODOvue component ecosystem
