Skip to content

chrisboulton/shadcn-components-table-view

Repository files navigation

shadcn-table-view

A feature-rich data table and view management component registry for shadcn/ui.

  • Sortable tables with pagination and remote/XHR content loading
  • Column visibility controls
  • Predefined and custom saveable views
  • Structured search with field suggestions, syntax highlighting, and query builder
  • Floating bulk actions bar with row checkboxes, select all, and shift-click support

Example

Quick Start

npx shadcn@latest add https://raw.githubusercontent.com/chrisboulton/shadcn-components-table-view/main/public/r/table-view.json

Or install individual components:

npx shadcn@latest add https://raw.githubusercontent.com/chrisboulton/shadcn-components-table-view/main/public/r/data-table.json
npx shadcn@latest add https://raw.githubusercontent.com/chrisboulton/shadcn-components-table-view/main/public/r/view-manager.json
npx shadcn@latest add https://raw.githubusercontent.com/chrisboulton/shadcn-components-table-view/main/public/r/search-command.json

Components

DataTable

Sortable table with pagination, row selection (with shift-click range selection), inline action buttons, and a dropdown actions menu.

<DataTable
  data={listResponse}
  columns={columns}
  columnConfig={columnConfig}
  selectedItems={selectedIds}
  onSelectionChange={setSelectedIds}
  onPageChange={setPage}
  onSortChange={(field, order) => setSort({ field, order })}
  onItemsPerPageChange={setPerPage}
  sortBy="name"
  sortOrder="asc"
  loading={isLoading}
  itemName="customers"
  getItemId={(item) => item.id}
  getItemDisplayName={(item) => item.name}
  renderEditLink={(item) => (
    <DropdownMenuItem asChild>
      <Link to={`/customers/${item.id}`}><Edit className="mr-2 h-4 w-4" />Edit</Link>
    </DropdownMenuItem>
  )}
  inlineActions={[
    { id: 'view', icon: Eye, label: 'View', onClick: (item) => navigate(`/customers/${item.id}`) },
  ]}
/>

Props

Prop Type Default Description
data ListResponse<T> — Page-based response with data, total, page, per_page, total_pages
columns DataTableColumn<T>[] — Column definitions with id, label, optional render function
columnConfig ColumnConfig[] — Column visibility configuration
selectedItems string[] [] Array of selected item IDs
onSelectionChange (ids: string[]) => void — Called when selection changes
onEdit (item: T) => void — Edit handler (shown in dropdown menu)
onDelete (itemId: string) => void — Delete handler (shown in dropdown menu)
onPageChange (page: number) => void — Required. Page change handler
onSortChange (field: TSortField, order: 'asc' | 'desc') => void — Sort change handler
onItemsPerPageChange (itemsPerPage: number) => void — Items per page change handler
sortBy TSortField — Current sort field
sortOrder 'asc' | 'desc' — Current sort direction
loading boolean false Show loading spinner
itemName string — Required. Plural noun for display (e.g. "customers")
getItemId (item: T) => string — Required. Extract unique ID from row item
getItemDisplayName (item: T) => string — Extract display name (used in aria labels)
additionalActions (item: T) => ReactNode — Extra dropdown menu items per row
renderEditLink (item: T) => ReactNode — Custom edit link (replaces default onEdit in dropdown)
inlineActions InlineAction<T>[] — Buttons rendered directly in the actions cell
hideDropdownActions boolean false Hide the "..." dropdown menu entirely
showCheckboxes boolean true Show row selection checkboxes
showSorting boolean true Show sortable column headers

ViewManager

Tabbed view switcher with integrated search, column toggle dropdown, and optional saved custom views (via a pluggable adapter).

<ViewManager
  currentView={currentView}
  builtInViews={views}
  searchQuery={searchQuery}
  searchFields={searchFields}
  entity="customers"
  storeId="store-1"
  onViewChange={setCurrentView}
  onSearchChange={setSearchQuery}
  onColumnToggle={handleColumnToggle}
  savedViewsAdapter={myAdapter}
/>

Props

Prop Type Default Description
currentView DataView<TSortField> — Required. Active view
builtInViews DataView<TSortField>[] — Required. Predefined views
searchQuery string — Required. Current search query
searchFields SearchField[] — Required. Fields available in the search command palette
entity string — Required. Entity type (e.g. "customers")
storeId string — Required. Store identifier
onViewChange (view: DataView<TSortField>) => void — Required. Called when the active view changes
onSearchChange (query: string) => void — Required. Called when search query changes
onColumnToggle (columnId: string) => void — Required. Called when a column is toggled
showSearch boolean true Show the search bar
showAllViewsInTabs boolean false Show all views as tabs (instead of first 2 + overflow dropdown)
allowCustomViews boolean true Enable "Save view" functionality
showColumnToggle boolean true Show column visibility dropdown
savedViewsAdapter SavedViewsAdapter — Adapter for CRUD on saved views. Custom views are disabled when omitted.

SearchCommand

Command palette for structured search queries with field suggestions, value autocomplete, recent search history, and syntax tips.

Props

Prop Type Default Description
searchFields SearchField[] — Required. Available search fields
searchQuery string — Required. Current query string
recentSearches string[] — Required. Recent search history
onQueryChange (query: string) => void — Required. Called when the query changes
onSelectField (field: SearchField) => void — Required. Called when a field is selected
onSelectValue (value: string, operator?: string) => void — Required. Called when a value is selected
onSelectSuggestion (query: string) => void — Required. Called when a recent search is selected
onSubmit () => void — Required. Called on Enter
enableHighlight boolean true Enable syntax highlighting in the input

SearchInput

A cmdk CommandInput wrapper that renders a transparent overlay with syntax-highlighted tokens. Used internally by SearchCommand.

Props

Prop Type Default Description
value string — Input value
onValueChange (value: string) => void — Value change handler
enableHighlight boolean false Enable syntax highlighting overlay
knownAttributeFields string[] — Attribute field names to highlight (without @ prefix)
className string — Additional CSS class

Also accepts all props from cmdk's CommandInput (e.g. placeholder, onFocus, onBlur, onKeyDown).


BulkActionsBar

Fixed-position floating bar that appears when rows are selected. Renders action buttons and a clear selection button.

<BulkActionsBar
  selectedCount={selectedIds.length}
  actions={[
    { icon: Mail, label: 'Email', onClick: handleBulkEmail },
    { icon: Trash2, label: 'Delete', onClick: handleBulkDelete, variant: 'destructive' },
  ]}
  onClearSelection={() => setSelectedIds([])}
  itemName="customers"
/>

Props

Prop Type Default Description
selectedCount number — Required. Number of selected items. Bar is hidden when 0.
actions BulkAction[] — Required. Action buttons
onClearSelection () => void — Required. Called when "Clear" is clicked
itemName string — Noun for display (e.g. "3 customers selected")

BulkAction

interface BulkAction {
  icon: LucideIcon
  label: string
  onClick: () => void
  variant?: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost'
}

TablePagination

Pagination controls with page numbers, prev/next, items-per-page selector, and "Showing X-Y of Z" text. Used internally by DataTable.

Props

Prop Type Default Description
currentPage number — Required. Current page (1-indexed)
totalPages number — Required. Total number of pages
onPageChange (page: number) => void — Required. Page change handler
totalItems number — Required. Total item count
itemsPerPage number — Required. Items per page
onItemsPerPageChange (itemsPerPage: number) => void — Items per page change handler. When provided, shows a page size selector.
itemName string "items" Noun for display text

Hooks

All hooks accept a SavedViewsAdapter (or an extended adapter) as a parameter — no hardcoded API layer.

useSavedViews

Fetch saved views for an entity.

function useSavedViews(
  entity: SavedViewEntity,
  adapter: SavedViewsAdapter,
  options?: Omit<UseQueryOptions<SavedView[]>, 'queryKey' | 'queryFn'>,
): UseQueryResult<SavedView[]>

useSavedView

Fetch a single saved view by ID. Requires an adapter with a get method.

function useSavedView(
  id: string,
  adapter: SavedViewsAdapter & { get?: (id: string) => Promise<SavedView> },
  options?: Omit<UseQueryOptions<SavedView>, 'queryKey' | 'queryFn'>,
): UseQueryResult<SavedView>

useCreateSavedView

function useCreateSavedView(
  adapter: SavedViewsAdapter,
): UseMutationResult<SavedView, Error, CreateSavedViewRequest>

useUpdateSavedView

Requires an adapter with an update method.

function useUpdateSavedView(
  adapter: SavedViewsAdapter & {
    update?: (id: string, data: UpdateSavedViewRequest) => Promise<SavedView>
  },
): UseMutationResult<SavedView, Error, { id: string; data: UpdateSavedViewRequest }>

useDeleteSavedView

function useDeleteSavedView(
  adapter: SavedViewsAdapter,
): UseMutationResult<{ id: string; entity: SavedViewEntity }, Error, { id: string; entity: SavedViewEntity }>

Types

ListResponse<T>

Page-based paginated response expected by DataTable.

interface ListResponse<T> {
  data: T[]
  total: number
  page: number         // 1-indexed
  per_page: number
  total_pages: number
  sort?: string
  order?: 'asc' | 'desc'
}

DataView<TSortField>

Represents a named table view with filters, sort, and column configuration.

interface DataView<TSortField = string> {
  id: string
  name: string
  description?: string
  isBuiltIn: boolean
  filters: ViewFilter
  sort: { field: TSortField; order: 'asc' | 'desc' }
  columns: ColumnConfig[]
}

DataTableColumn<T>

Column definition for DataTable.

interface DataTableColumn<T = any> {
  id: string
  label: string
  sortable?: boolean
  render?: (item: T) => React.ReactNode
  className?: string
}

ColumnConfig

Column visibility entry (used in DataView.columns and passed to DataTable.columnConfig).

interface ColumnConfig {
  id: string
  label: string
  visible: boolean
  width?: string
}

SearchField

Describes a field available for search. Supports text, boolean, date, number, and combobox types.

interface SearchField {
  id: string            // Use "@attr" prefix for attribute fields
  label: string
  type: 'text' | 'boolean' | 'date' | 'number' | 'combobox'
  operators?: string[]
  options?: { value: string; label: string }[]
  searchCallback?: (storeId: string, searchTerm: string) => Promise<Array<{ id: string; name: string }>>
}

InlineAction<T>

An action rendered as a button directly in the row's actions cell.

interface InlineAction<T = any> {
  id: string
  icon: React.ComponentType<{ className?: string }>
  label: string
  showLabel?: boolean   // default: false
  onClick: (item: T) => void
  disabled?: (item: T) => boolean
  variant?: 'default' | 'secondary' | 'outline' | 'ghost' | 'destructive'
  className?: string
}

ViewFilter

interface ViewFilter {
  search: string
  [key: string]: any
}

SavedView

Persisted view returned by the adapter.

interface SavedView {
  id: string
  name: string
  entity: SavedViewEntity     // string
  visible_by: SavedViewVisibility  // 'everyone' | 'owner'
  search_query?: string
  sort_by: string
  sort_direction: 'asc' | 'desc'
  columns: string[]
  per_page: number
}

SavedViewsAdapter

Pluggable persistence interface. Implement this to connect saved views to your API.

interface SavedViewsAdapter {
  list: (entity: string) => Promise<SavedView[]>
  create: (data: CreateSavedViewRequest) => Promise<SavedView>
  delete: (id: string) => Promise<void>
}

Optionally extend with get and update for useSavedView / useUpdateSavedView:

interface ExtendedAdapter extends SavedViewsAdapter {
  get: (id: string) => Promise<SavedView>
  update: (id: string, data: UpdateSavedViewRequest) => Promise<SavedView>
}

Utilities

Pagination

Convert between offset-based API responses and the page-based ListResponse format.

transformToListResponse<T>(response: OffsetPaginationResponse<T>): ListResponse<T>
pageToOffset(page: number, perPage: number): number
offsetToPage(offset: number, perPage: number): number

OffsetPaginationResponse<T> shape:

{ data: T[], pagination: { total: number, limit: number, offset: number, has_more: boolean } }

Search History

LocalStorage-backed per-entity search history (max 10 items).

getSearchHistory(entity: string): string[]
addToSearchHistory(entity: string, query: string): void
clearSearchHistory(entity: string): void
removeFromSearchHistory(entity: string, query: string): void

Search Query Parser

Parse and manipulate structured field:value queries.

parseSearchQuery(query: string, cursorPosition: number): ParseResult
buildFilterString(field: string, value: string, operator?: string): string
updateQueryWithFilter(query: string, cursorPosition: number, filterString: string): { query: string; cursorPosition: number }

Syntax Highlighter

Tokenize queries for syntax-highlighted rendering.

tokenizeForHighlight(query: string, knownAttributeFields?: string[]): HighlightToken[]
getTokenClassName(type: TokenType): string

Token types: field, colon, operator, value, keyword, paren, wildcard, text.


Search Query Syntax

The search system supports structured queries with the following syntax:

Syntax Example Description
field:value name:John Exact match
field:*value* email:*@gmail.com Wildcard (contains, starts/ends with)
field:>value score:>90 Greater than (also <, >=, <=, =)
field:>date created_at:>2024-01-01 Date comparison
field:"val ue" name:"John Doe" Quoted value with spaces
@attr:value @plan:enterprise Attribute field
AND / OR / NOT name:John AND status:active Boolean operators
( ) (name:John OR name:Jane) AND active:true Grouping
-field:value -status:archived Negation

Usage Examples

Full-featured table with view management and bulk actions

import {
  DataTable,
  ViewManager,
  BulkActionsBar,
  type DataView,
  type ListResponse,
  type DataTableColumn,
  type SearchField,
} from 'shadcn-table-view'

function CustomersPage() {
  const [currentView, setCurrentView] = useState<DataView>(builtInViews[0])
  const [searchQuery, setSearchQuery] = useState('')
  const [selectedIds, setSelectedIds] = useState<string[]>([])
  const [page, setPage] = useState(1)

  const views: DataView[] = [
    {
      id: 'all',
      name: 'All Customers',
      isBuiltIn: true,
      filters: { search: '' },
      sort: { field: 'name', order: 'asc' },
      columns: [
        { id: 'name', label: 'Name', visible: true },
        { id: 'email', label: 'Email', visible: true },
        { id: 'status', label: 'Status', visible: true },
      ],
    },
  ]

  const searchFields: SearchField[] = [
    { id: 'name', label: 'Name', type: 'text' },
    { id: 'email', label: 'Email', type: 'text' },
    { id: 'status', label: 'Status', type: 'combobox', options: [
      { value: 'active', label: 'Active' },
      { value: 'inactive', label: 'Inactive' },
    ]},
  ]

  const columns: DataTableColumn<Customer>[] = [
    { id: 'name', label: 'Name', sortable: true },
    { id: 'email', label: 'Email', sortable: true },
    { id: 'status', label: 'Status', render: (item) => <Badge>{item.status}</Badge> },
  ]

  return (
    <>
      <ViewManager
        currentView={currentView}
        builtInViews={views}
        searchQuery={searchQuery}
        searchFields={searchFields}
        entity="customers"
        storeId="store-1"
        onViewChange={setCurrentView}
        onSearchChange={setSearchQuery}
        onColumnToggle={(id) => {/* toggle column visibility */}}
        savedViewsAdapter={mySavedViewsAdapter}
      />
      <DataTable
        data={data}
        columns={columns}
        columnConfig={currentView.columns}
        selectedItems={selectedIds}
        onSelectionChange={setSelectedIds}
        onPageChange={setPage}
        onSortChange={(field, order) => {/* update sort */}}
        itemName="customers"
        getItemId={(item) => item.id}
      />
      <BulkActionsBar
        selectedCount={selectedIds.length}
        actions={[
          { icon: Mail, label: 'Email', onClick: () => {/* bulk email */} },
          { icon: Trash2, label: 'Delete', onClick: () => {/* bulk delete */}, variant: 'destructive' },
        ]}
        onClearSelection={() => setSelectedIds([])}
        itemName="customers"
      />
    </>
  )
}

Read-only table with inline actions

<ViewManager
  currentView={currentView}
  builtInViews={views}
  searchQuery={searchQuery}
  searchFields={searchFields}
  entity="orders"
  storeId="store-1"
  onViewChange={setCurrentView}
  onSearchChange={setSearchQuery}
  onColumnToggle={handleColumnToggle}
  allowCustomViews={false}
/>
<DataTable
  data={data}
  columns={columns}
  columnConfig={currentView.columns}
  onPageChange={setPage}
  itemName="orders"
  getItemId={(item) => item.id}
  showCheckboxes={false}
  hideDropdownActions
  inlineActions={[
    { id: 'view', icon: Eye, label: 'View', onClick: (item) => navigate(`/orders/${item.id}`) },
  ]}
/>

Simple standalone table (no ViewManager)

<DataTable
  data={data}
  columns={[
    { id: 'name', label: 'Name', sortable: true },
    { id: 'email', label: 'Email' },
  ]}
  columnConfig={[
    { id: 'name', label: 'Name', visible: true },
    { id: 'email', label: 'Email', visible: true },
  ]}
  onPageChange={setPage}
  onSortChange={setSortField}
  sortBy={sortBy}
  sortOrder={sortOrder}
  itemName="users"
  getItemId={(item) => item.id}
  showCheckboxes={false}
  hideDropdownActions
/>

Requirements

  • React 19+, Tailwind CSS v4, shadcn/ui
  • @tanstack/react-query (for useSavedViews hooks)

Development

npm install
npm run dev          # Start example at localhost:5173
npm run typecheck    # Type checking
npm run lint         # ESLint
npm run registry:build  # Build registry JSON files

License

MIT

About

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published