ResponsiveDataExplorer
A complete local-data workflow that stays a semantic table on desktop, becomes usable cards on mobile, and makes filtering, recovery, selection, and navigation state explicit.
Installation
The CLI installs editable source, AsyncState, and the compatible TanStack Table dependency. Elsecase itself is not a runtime package.
pnpm dlx shadcn@latest add https://elsecase.vercel.app/r/data-explorer.jsonBasic usage
import {
ResponsiveDataExplorer,
createDataExplorerColumnHelper,
} from "@/components/data-explorer"
const helper = createDataExplorerColumnHelper<User>()
const columns = helper.columns([
helper.accessor("name", { header: "Name" }),
helper.accessor("role", { header: "Role" }),
])
<ResponsiveDataExplorer
data={users}
columns={columns}
getRowId={(user) => user.id}
search={{ getSearchText: (user) => user.name }}
mobileCard={(user) => <UserCard user={user} />}
syncStateToUrl
/>API reference
| Prop | Purpose | Default |
|---|---|---|
data / columns / getRowId | Rows, v9 column definitions, and stable identity. | Required |
mobileCard | Accessible narrow-screen representation of one row. | Required |
status / error / onRetry | AsyncState integration for request conditions. | Derived success/empty |
search | Label, placeholder, and searchable-text accessor. | None |
filters | Named select filters with options and value accessors. | [] |
pagination / sorting / selection | Optional controlled state slices. | Internally owned |
on*Change | Resolved controlled-state callbacks. | None |
bulkActions | Surface mounted only while at least one row is selected. | None |
emptyState / noResultsState | Distinct zero-data and zero-match views. | Built-in views |
syncStateToUrl | Restores and writes supported explorer query state. | false |
States and edge cases
The example covers initial loading, stale-content refreshing, empty source data, no matching results, request failure, offline recovery, long emails, multiple pages, and selected rows. Search and filter changes always return to the first page.
Accessibility behavior
Desktop output uses native table, header, and cell semantics. Sorting is operated by real buttons and exposed through aria-sort. Selection uses named native checkboxes, bulk-action changes are announced, and all controls meet a 44px minimum target. Async conditions inherit AsyncState’s live-region behavior.
Responsive behavior
At the medium breakpoint, the desktop table is removed from layout and the same paginated rows are rendered as consumer-defined cards. The mobile renderer receives the original typed row, so it can prioritize information instead of squeezing columns into a narrow table.
URL state
Search, filters, sorting, page, and page size use readable query keys. Unrelated parameters are preserved, defaults are omitted, and the component listens for browser back and forward navigation. This logic uses the browser History API, so the registry component is not coupled to Next.js.
Testing example
fireEvent.change(screen.getByRole("searchbox"), {
target: { value: "ada" },
})
expect(screen.getByText("Ada Lovelace")).toBeInTheDocument()
expect(screen.queryByText("Grace Hopper")).not.toBeInTheDocument()Customization
<ResponsiveDataExplorer
{...props}
emptyState={<InviteFirstMember />}
noResultsState={<SavedSearchEmpty />}
bulkActions={<MemberBulkActions />}
mobileCard={(user) => <CompactUserCard user={user} />}
/>Dependencies
React, AsyncState, and @tanstack/react-table@^9.1.2. The component intentionally targets TanStack Table v9 rather than its deprecated v8 compatibility adapter.
Known limitations
Version 0.1 processes local rows. Controlled sorting and pagination are designed to feed server requests, but fetching and cache ownership stay with the application. URL keys are intentionally human-readable, so use one synchronized explorer per route unless you scope it in a wrapper.
Source
Review the implementation and behavior tests on GitHub:
github.com/heysinghaaa/Elsecase/tree/main/registry/data-explorer