@alsocoder/apna-table
v0.1.2
Published
A flexible React data table with client/server modes, sorting, filters, pagination, row selection, and bulk actions.
Maintainers
Readme
@alsocoder/apna-table
A flexible React data table with client and server modes, multi-column sorting, filters (ApnaInput / ApnaSelect / ApnaDatePicker), pagination, row selection, bulk actions, skeleton loading, and declarative row actions.
Install
npm install @alsocoder/apna-tableFilter/pagination UI internally uses ApnaInput, ApnaSelect, and ApnaDatePicker — you do not need to install or import them separately.
CSS import
import "@alsocoder/apna-table/styles.css"This single stylesheet includes table styles plus the bundled filter/pagination component styles.
Quick start
import { ApnaTable, type ApnaTableColumn } from "@alsocoder/apna-table"
import "@alsocoder/apna-table/styles.css"
type User = { id: string; name: string; email: string }
const columns: ApnaTableColumn<User>[] = [
{ id: "name", header: "Name", accessorKey: "name", sortable: true },
{ id: "email", header: "Email", accessorKey: "email", sortable: true },
]
<ApnaTable
mode="client"
title="Users"
description="Manage users"
columns={columns}
rowKey="id"
data={users}
showSerialNumber
selectable
filters={[
{
key: "search",
type: "input",
placeholder: "Search…",
value: search,
onValueChange: setSearch,
},
]}
actions={{
variant: "dropdown",
position: "end",
items: [
{ key: "view", label: "View", icon: <EyeIcon />, onAction: (row) => view(row) },
],
}}
headerActions={<button>Add user</button>}
/>Modes
Client mode
Pass data once. The table handles local pagination and sorting.
<ApnaTable mode="client" data={items} columns={columns} rowKey="id" />Server mode
Pass fetchData. The table sends page, pageSize, sort, and debounced filters.
<ApnaTable
mode="server"
fetchData={async ({ page, pageSize, sort, filters, signal }) => {
const res = await listUsers({ page, pageSize, sort, ...filters }, { signal })
return { items: res.items, total: res.total }
}}
columns={columns}
rowKey="id"
/>Columns
type ApnaTableColumn<T> = {
id: string
header: ReactNode
accessorKey?: keyof T
cell?: (ctx: { row: T; index: number; serial: number }) => ReactNode
sortable?: boolean
sortKey?: string
hidden?: boolean | "sm" | "md" | "lg"
align?: "left" | "center" | "right"
}Column order:
| Feature | Position |
|---------|----------|
| S.No | First (optional) |
| Checkbox | After S.No (optional) |
| Actions (position: "start") | After checkbox |
| User columns | Middle |
| Actions (position: "end") | Last (default) |
Filters
Supported filter types:
| type | Component |
|------|-----------|
| input | ApnaInput |
| select | ApnaSelect |
| date | ApnaDatePicker |
| dateRange | ApnaDateRangePicker |
First filter stays visible. Extra filters open via Show filters.
Row actions
Dropdown (ellipsis menu)
actions={{
variant: "dropdown",
position: "end",
items: [
{ key: "edit", label: "Edit", icon: <EditIcon />, onAction: editRow },
{ key: "delete", label: "Delete", icon: <TrashIcon />, destructive: true, onAction: deleteRow },
],
}}Inline (side-by-side buttons)
actions={{
variant: "inline",
position: "start",
items: [
{ key: "edit", label: "Edit", icon: <EditIcon />, iconOnly: true, onAction: editRow },
],
}}show: falsehides the actions column- Each action supports
label+ optionalicon(consumer-provided ReactNode)
Bulk actions
Shown in the toolbar when rows are selected:
bulkActions={[
{
key: "archive",
label: "Archive selected",
icon: <ArchiveIcon />,
onAction: (rows, keys) => archiveMany(rows),
},
]}Sorting
type ApnaTableSort = { field: string; direction: "asc" | "desc" }- Click a sortable header to cycle
asc → desc → none multiSortenables multiple sort columns- Server mode sends
sort=createdAt:desc,title:asc
Skeleton loading
- Server mode: skeleton shows while
fetchDatais in flight (after filter debounce) - Client mode: brief skeleton while filters/sort/page apply
skeletonRowscontrols placeholder row count (defaults topageSize)- Pagination is disabled while
loadingis true
Backend API contract
Request query params
| Param | Example |
|-------|---------|
| page | 1 |
| pageSize | 25 |
| sort | createdAt:desc,title:asc |
| filter keys | search=foo&status=active&dateFrom=2026-01-01&dateTo=2026-01-31 |
Date range filters are sent as {key}From and {key}To.
Response shape
{
"success": true,
"data": [],
"meta": { "page": 1, "pageSize": 25, "total": 142, "totalPages": 6 }
}Node/Express + MongoDB example
function parseSort(sort?: string): Record<string, 1 | -1> {
if (!sort) return { createdAt: -1 }
return Object.fromEntries(
sort.split(",").map((part) => {
const [field, dir] = part.split(":")
return [field, dir === "asc" ? 1 : -1]
})
)
}
export async function listUsers({ page, pageSize, search, status, sort }) {
const filter: Record<string, unknown> = {}
if (search) filter.$or = [{ name: new RegExp(search, "i") }, { email: new RegExp(search, "i") }]
if (status && status !== "all") filter.status = status
const skip = (page - 1) * pageSize
const [total, items] = await Promise.all([
User.countDocuments(filter),
User.find(filter).sort(parseSort(sort)).skip(skip).limit(pageSize),
])
return { items, total, page, pageSize }
}Hooks
import { useApnaTable, useApnaTablePagination, buildSortQuery } from "@alsocoder/apna-table"
const table = useApnaTable({ mode: "server", fetchData, columns, rowKey: "id" })
// rows, loading, pagination, sort, selection, refetchCustomization
- Vanilla CSS with
--apna-table-*variables - Falls back to shadcn/Tailwind tokens (
--border,--foreground,--primary, etc.) classNamesobject +className/tableClassName/headerClassNameshortcutsiconsprop to override built-in SVG icons
Playground
cd playground
npm install
npm run devMahima migration
Before:
<DataTable title="Services" filters={filters} pagination={pagination}>
<Table>...</Table>
</DataTable>After:
<ApnaTable
mode="server"
title="Services"
columns={columns}
fetchData={listServices}
filters={filters}
showSerialNumber
actions={{ variant: "dropdown", items: [...] }}
headerActions={<Button>Add service</Button>}
/>License
MIT
