Search Table
Add global search functionality to quickly find data across all columns.
Loading data...
"use client"
import React from "react"import { DataTableRoot, DataTable, DataTableHeader, DataTableBody, DataTableEmptyBody, DataTableSkeleton,} from "@/components/niko-table/core"import { DataTableToolbarSection, DataTablePagination, DataTableSearchFilter, DataTableViewMenu, DataTableEmptyIcon, DataTableEmptyMessage, DataTableEmptyFilteredMessage, DataTableEmptyTitle, DataTableEmptyDescription, DataTableColumnTitle, DataTableColumnHeader, DataTableColumnSortMenu,} from "@/components/niko-table/components"import type { DataTableColumnDef } from "@/components/niko-table/types"import { Button } from "@/components/ui/button"import { UserSearch, SearchX } from "lucide-react"
type Customer = { id: string name: string email: string company: string phone: string}
const columns: DataTableColumnDef<Customer>[] = [ { accessorKey: "name", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Name", }, }, { accessorKey: "email", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Email", }, }, { accessorKey: "company", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Company", }, }, { accessorKey: "phone", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Phone", }, },]
const mockData: Customer[] = [ { id: "1", name: "John Doe", company: "Acme Corp", phone: "555-0100", }, { id: "2", name: "Jane Smith", company: "TechCo", phone: "555-0101", }, { id: "3", name: "Bob Johnson", company: "StartUp Inc", phone: "555-0102", }, { id: "4", name: "Alice Williams", company: "DesignCo", phone: "555-0103", }, { id: "5", name: "Charlie Brown", company: "Consulting LLC", phone: "555-0104", }, { id: "6", name: "Diana Prince", company: "Enterprise Inc", phone: "555-0105", }, { id: "7", name: "Ethan Hunt", company: "Mission Impossible", phone: "555-0106", }, { id: "8", name: "Fiona Green", company: "GreenTech", phone: "555-0107", }, { id: "9", name: "George Miller", company: "Media Corp", phone: "555-0108", }, { id: "10", name: "Hannah Lee", company: "Innovation Labs", phone: "555-0109", },]
// Simulate API callfunction fetchData(delay = 1500): Promise<Customer[]> { return new Promise(resolve => { setTimeout(() => { resolve(mockData) }, delay) })}
export default function SearchTableExample() { const [data, setData] = React.useState<Customer[]>([]) const [isLoading, setIsLoading] = React.useState(true)
// Simulate initial data fetch React.useEffect(() => { setIsLoading(true) fetchData() .then(setData) .finally(() => setIsLoading(false)) }, [])
// Simulate refetch for demo purposes const handleRefresh = () => { setIsLoading(true) fetchData(1000) .then(setData) .finally(() => setIsLoading(false)) }
return ( <div className="w-full space-y-4"> <div className="flex items-center justify-between"> <div className="text-sm text-muted-foreground"> {isLoading ? "Loading data..." : `Showing ${data.length} customers`} </div> <Button onClick={handleRefresh} disabled={isLoading} size="sm"> {isLoading ? "Loading..." : "Refresh Data"} </Button> </div>
<DataTableRoot data={data} columns={columns} isLoading={isLoading}> <DataTableToolbarSection className="justify-between"> <DataTableSearchFilter placeholder="Search anything..." /> <DataTableViewMenu /> </DataTableToolbarSection> <DataTable> <DataTableHeader /> <DataTableBody> <DataTableSkeleton rows={5} /> <DataTableEmptyBody> <DataTableEmptyMessage> <DataTableEmptyIcon> <UserSearch className="size-12" /> </DataTableEmptyIcon> <DataTableEmptyTitle>No customers found</DataTableEmptyTitle> <DataTableEmptyDescription> There are no customers to display at this time. </DataTableEmptyDescription> </DataTableEmptyMessage> <DataTableEmptyFilteredMessage> <DataTableEmptyIcon> <SearchX className="size-12" /> </DataTableEmptyIcon> <DataTableEmptyTitle>No matches found</DataTableEmptyTitle> <DataTableEmptyDescription> Try adjusting your search to find what you're looking for. </DataTableEmptyDescription> </DataTableEmptyFilteredMessage> </DataTableEmptyBody> </DataTableBody> </DataTable> <DataTablePagination pageSizeOptions={[5, 10, 20]} /> </DataTableRoot> </div> )}Preview with Controlled State
Loading data...
Search Table State
Live view of the search table state with loading and data management
Loading State:Loading...
Search Query:None
Total Customers:0
Filtered Results:0
Unique Companies:0
Email Domains:0
Sorting:None
Page:1 (Size: 5)
Hidden Columns:0
View Full State Object
Loading State:
{
"isLoading": true,
"dataLength": 0
}Global Filter:
""
Sorting:
[]
Pagination:
{
"pageIndex": 0,
"pageSize": 5
}Column Visibility:
{}"use client"
import React, { useState } from "react"import type { PaginationState, SortingState, ColumnFiltersState, VisibilityState,} from "@tanstack/react-table"import { DataTableRoot, DataTable, DataTableHeader, DataTableBody, DataTableEmptyBody, DataTableSkeleton,} from "@/components/niko-table/core"import { DataTableToolbarSection, DataTablePagination, DataTableSearchFilter, DataTableViewMenu, DataTableEmptyIcon, DataTableEmptyMessage, DataTableEmptyFilteredMessage, DataTableEmptyTitle, DataTableEmptyDescription, DataTableColumnTitle, DataTableColumnHeader, DataTableColumnSortMenu,} from "@/components/niko-table/components"import type { DataTableColumnDef } from "@/components/niko-table/types"import { Button } from "@/components/ui/button"import { UserSearch, SearchX } from "lucide-react"import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle,} from "@/components/ui/card"
type Customer = { id: string name: string email: string company: string phone: string}
const columns: DataTableColumnDef<Customer>[] = [ { accessorKey: "name", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Name", }, }, { accessorKey: "email", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Email", }, }, { accessorKey: "company", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Company", }, }, { accessorKey: "phone", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Phone", }, },]
const mockData: Customer[] = [ { id: "1", name: "John Doe", company: "Acme Corp", phone: "555-0100", }, { id: "2", name: "Jane Smith", company: "TechCo", phone: "555-0101", }, { id: "3", name: "Bob Johnson", company: "StartUp Inc", phone: "555-0102", }, { id: "4", name: "Alice Williams", company: "DesignCo", phone: "555-0103", }, { id: "5", name: "Charlie Brown", company: "Consulting LLC", phone: "555-0104", }, { id: "6", name: "Diana Prince", company: "Enterprise Inc", phone: "555-0105", }, { id: "7", name: "Ethan Hunt", company: "Mission Impossible", phone: "555-0106", }, { id: "8", name: "Fiona Green", company: "GreenTech", phone: "555-0107", }, { id: "9", name: "George Miller", company: "Media Corp", phone: "555-0108", }, { id: "10", name: "Hannah Lee", company: "Innovation Labs", phone: "555-0109", },]
// Simulate API callfunction fetchData(delay = 1500): Promise<Customer[]> { return new Promise(resolve => { setTimeout(() => { resolve(mockData) }, delay) })}
export default function SearchTableStateExample() { // Controlled state management for all table state const [globalFilter, setGlobalFilter] = useState<string | object>("") const [sorting, setSorting] = useState<SortingState>([]) const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]) const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({}) const [pagination, setPagination] = useState<PaginationState>({ pageIndex: 0, pageSize: 5, })
// Loading and data state const [data, setData] = useState<Customer[]>([]) const [isLoading, setIsLoading] = useState(true)
// Simulate initial data fetch React.useEffect(() => { setIsLoading(true) fetchData() .then(setData) .finally(() => setIsLoading(false)) }, [])
// Simulate refetch for demo purposes const handleRefresh = () => { setIsLoading(true) fetchData(1000) .then(setData) .finally(() => setIsLoading(false)) }
const resetAllState = () => { setGlobalFilter("") setSorting([]) setColumnFilters([]) setColumnVisibility({}) setPagination({ pageIndex: 0, pageSize: 5 }) }
// Calculate filtered data for display metrics const filteredRowCount = React.useMemo(() => { if (!globalFilter || !data.length) return data.length
const filtered = data.filter(item => { if (typeof globalFilter !== "string") return true return Object.values(item).some(value => String(value).toLowerCase().includes(globalFilter.toLowerCase()), ) }) return filtered.length }, [data, globalFilter])
// Calculate loading and data metrics const dataMetrics = React.useMemo(() => { const uniqueCompanies = Array.from( new Set(data.map(customer => customer.company)), ) const emailDomains = Array.from( new Set(data.map(customer => customer.email.split("@")[1])), )
return { totalCustomers: data.length, uniqueCompanies: uniqueCompanies.length, emailDomains: emailDomains.length, companies: uniqueCompanies, } }, [data])
return ( <div className="w-full space-y-4"> <div className="flex items-center justify-between"> <div className="text-sm text-muted-foreground"> {isLoading ? "Loading data..." : `Showing ${data.length} customers`} </div> <Button onClick={handleRefresh} disabled={isLoading} size="sm"> {isLoading ? "Loading..." : "Refresh Data"} </Button> </div>
<DataTableRoot data={data} columns={columns} isLoading={isLoading} state={{ globalFilter, sorting, columnFilters, columnVisibility, pagination, }} onGlobalFilterChange={value => { setGlobalFilter(value) setPagination(prev => ({ ...prev, pageIndex: 0 })) }} onSortingChange={setSorting} onColumnFiltersChange={filters => { setColumnFilters(filters) setPagination(prev => ({ ...prev, pageIndex: 0 })) }} onColumnVisibilityChange={setColumnVisibility} onPaginationChange={setPagination} > <DataTableToolbarSection className="justify-between"> <DataTableSearchFilter placeholder="Search anything..." /> <DataTableViewMenu /> </DataTableToolbarSection> <DataTable> <DataTableHeader /> <DataTableBody> <DataTableSkeleton rows={5} /> <DataTableEmptyBody> <DataTableEmptyMessage> <DataTableEmptyIcon> <UserSearch className="size-12" /> </DataTableEmptyIcon> <DataTableEmptyTitle>No customers found</DataTableEmptyTitle> <DataTableEmptyDescription> There are no customers to display at this time. </DataTableEmptyDescription> </DataTableEmptyMessage> <DataTableEmptyFilteredMessage> <DataTableEmptyIcon> <SearchX className="size-12" /> </DataTableEmptyIcon> <DataTableEmptyTitle>No matches found</DataTableEmptyTitle> <DataTableEmptyDescription> Try adjusting your search to find what you're looking for. </DataTableEmptyDescription> </DataTableEmptyFilteredMessage> </DataTableEmptyBody> </DataTableBody> </DataTable> <DataTablePagination pageSizeOptions={[5, 10, 20]} /> </DataTableRoot>
{/* State Display for demonstration */} <Card> <CardHeader> <CardTitle>Search Table State</CardTitle> <CardDescription> Live view of the search table state with loading and data management </CardDescription> <CardAction> <Button variant="outline" size="sm" onClick={resetAllState}> Reset All State </Button> </CardAction> </CardHeader> <CardContent className="space-y-4"> <div className="grid gap-2 text-xs text-muted-foreground"> <div className="flex justify-between"> <span className="font-medium">Loading State:</span> <span className="text-foreground"> {isLoading ? "Loading..." : "Loaded"} </span> </div>
<div className="flex justify-between"> <span className="font-medium">Search Query:</span> <span className="text-foreground"> {typeof globalFilter === "string" ? globalFilter || "None" : "Mixed Filters"} </span> </div>
<div className="flex justify-between"> <span className="font-medium">Total Customers:</span> <span className="text-foreground"> {dataMetrics.totalCustomers} </span> </div>
<div className="flex justify-between"> <span className="font-medium">Filtered Results:</span> <span className="text-foreground">{filteredRowCount}</span> </div>
<div className="flex justify-between"> <span className="font-medium">Unique Companies:</span> <span className="text-foreground"> {dataMetrics.uniqueCompanies} </span> </div>
<div className="flex justify-between"> <span className="font-medium">Email Domains:</span> <span className="text-foreground"> {dataMetrics.emailDomains} </span> </div>
<div className="flex justify-between"> <span className="font-medium">Sorting:</span> <span className="text-foreground"> {sorting.length > 0 ? sorting .map(s => `${s.id} ${s.desc ? "desc" : "asc"}`) .join(", ") : "None"} </span> </div>
<div className="flex justify-between"> <span className="font-medium">Page:</span> <span className="text-foreground"> {pagination.pageIndex + 1} (Size: {pagination.pageSize}) </span> </div>
<div className="flex justify-between"> <span className="font-medium">Hidden Columns:</span> <span className="text-foreground"> { Object.values(columnVisibility).filter(v => v === false) .length } </span> </div> </div>
{/* Detailed state (collapsible) */} <details className="border-t pt-4"> <summary className="cursor-pointer text-xs font-medium hover:text-foreground"> View Full State Object </summary> <div className="mt-4 space-y-3 text-xs"> <div> <strong>Loading State:</strong> <pre className="mt-1 overflow-auto rounded bg-muted p-2"> {JSON.stringify( { isLoading, dataLength: data.length }, null, 2, )} </pre> </div> <div> <strong>Global Filter:</strong> <pre className="mt-1 overflow-auto rounded bg-muted p-2"> {JSON.stringify(globalFilter, null, 2)} </pre> </div> <div> <strong>Sorting:</strong> <pre className="mt-1 overflow-auto rounded bg-muted p-2"> {JSON.stringify(sorting, null, 2)} </pre> </div> <div> <strong>Pagination:</strong> <pre className="mt-1 overflow-auto rounded bg-muted p-2"> {JSON.stringify(pagination, null, 2)} </pre> </div> <div> <strong>Column Visibility:</strong> <pre className="mt-1 overflow-auto rounded bg-muted p-2"> {JSON.stringify(columnVisibility, null, 2)} </pre> </div> </div> </details> </CardContent> </Card> </div> )}Introduction
Section titled “Introduction”The Search Table adds a global search input that filters data across all columns. Users can quickly find what they’re looking for without complex filters.
Installation
Section titled “Installation”- Add the required components:
npx shadcn@latest add table input button dropdown-menu- Add
tanstack/react-tabledependency:
npm install @tanstack/react-table- Copy the DataTable components into your project. See the Installation Guide for detailed instructions.
Prerequisites
Section titled “Prerequisites”We are going to build a table to show customers. Here’s what our data looks like:
type Customer = { id: string name: string email: string company: string phone: string}
const data: Customer[] = [ { id: "1", name: "John Doe", company: "Acme Corp", phone: "+1 234-567-8900", }, // ...]Basic Table with Search
Section titled “Basic Table with Search”Let’s start by building a table with global search.
Column Definitions
Section titled “Column Definitions”First, we’ll define our columns.
"use client"
import { DataTableColumnHeader, DataTableColumnTitle, DataTableColumnSortMenu,} from "@/components/niko-table/components"import type { DataTableColumnDef } from "@/components/niko-table/types"
export type Customer = { id: string name: string email: string company: string phone: string}
export const columns: DataTableColumnDef<Customer>[] = [ { accessorKey: "name", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Name" }, }, { accessorKey: "email", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Email" }, }, { accessorKey: "company", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Company" }, }, { accessorKey: "phone", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Phone" }, },]<DataTable /> component
Section titled “<DataTable /> component”Next, we’ll add the search filter component.
"use client"
import { DataTableRoot, DataTable, DataTableHeader, DataTableBody,} from "@/components/niko-table/core"import { DataTableToolbarSection, DataTablePagination, DataTableSearchFilter, DataTableColumnHeader, DataTableColumnTitle, DataTableColumnSortMenu,} from "@/components/niko-table/components"import type { DataTableColumnDef } from "@/components/niko-table/types"
type Customer = { id: string name: string email: string company: string phone: string}
const columns: DataTableColumnDef<Customer>[] = [ { accessorKey: "name", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Name" }, }, { accessorKey: "email", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Email" }, }, { accessorKey: "company", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Company" }, }, { accessorKey: "phone", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Phone" }, },]
export function SearchTable({ data }: { data: Customer[] }) { return ( <DataTableRoot data={data} columns={columns} config={{ enablePagination: true, enableSorting: true, enableFilters: true, initialPageSize: 10, }} > <DataTableToolbarSection> <DataTableSearchFilter placeholder="Search customers..." /> </DataTableToolbarSection>
<DataTable> <DataTableHeader /> <DataTableBody /> </DataTable>
<DataTablePagination /> </DataTableRoot> )}How Search Works
Section titled “How Search Works”The global search filter works by:
- Converting all searchable column values to strings
- Searching for the query across all columns
- Returning rows where ANY column matches
- Case-insensitive matching by default
Controlling Searchable Columns
Section titled “Controlling Searchable Columns”By default, all columns are searchable. To exclude columns:
{ accessorKey: "id", header: "ID", enableColumnFilter: false, // Not searchable}Controlled Search State
Section titled “Controlled Search State”Manage search state externally:
import { useState } from "react"
export function ControlledSearchTable({ data }: { data: Customer[] }) { const [searchValue, setSearchValue] = useState("")
return ( <DataTableRoot data={data} columns={columns} config={{ enableFilters: true, }} onGlobalFilterChange={setSearchValue} > <DataTableToolbarSection> <DataTableSearchFilter placeholder="Search customers..." value={searchValue} onChange={setSearchValue} /> </DataTableToolbarSection>
<DataTable> <DataTableHeader /> <DataTableBody /> </DataTable>
<DataTablePagination /> </DataTableRoot> )}Custom Search Input
Section titled “Custom Search Input”Build your own search UI using the context:
import { useDataTable } from "@/components/niko-table/core"import { Input } from "@/components/ui/input"import { Search, X } from "lucide-react"import { Button } from "@/components/ui/button"
function CustomSearchInput() { const { table } = useDataTable<Customer>() const globalFilter = table.getState().globalFilter
return ( <div className="relative"> <Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> <Input value={(globalFilter as string) ?? ""} onChange={e => table.setGlobalFilter(e.target.value)} placeholder="Search..." className="pr-9 pl-9" /> {globalFilter && ( <Button variant="ghost" size="sm" className="absolute top-1/2 right-1 h-7 w-7 -translate-y-1/2 p-0" onClick={() => table.setGlobalFilter("")} > <X className="h-4 w-4" /> </Button> )} </div> )}When to Use
Section titled “When to Use”✅ Use Search Table when:
- Users need quick, simple searching
- Data is text-heavy (names, descriptions, etc.)
- You want a clean, minimal UI
- Dataset is < 1000 rows (client-side)
❌ Consider other options when:
- You need column-specific filters (use Advanced Table)
- Dataset is > 1000 rows (implement server-side filtering)
- Search is not the primary use case (use Basic Table)
Next Steps
Section titled “Next Steps”- Advanced Table - Add advanced filtering and sorting menus