Skip to content

Data Tables and Lists

import { createSnapshot } from "@lastshotlabs/snapshot";
import { DataTableBase } from "@lastshotlabs/snapshot/ui";
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
const snap = createSnapshot({ apiUrl: "/api" });
function UsersTable() {
const { data: users, isLoading, error } = useQuery<User[]>({
queryKey: ["/users"],
queryFn: () => snap.api.get("/users"),
});
const [sort, setSort] = useState({ column: "name", direction: "asc" });
return (
<DataTableBase
columns={[
{ field: "name", label: "Name", sortable: true },
{ field: "email", label: "Email" },
{ field: "role", label: "Role", format: "badge", badgeColors: {
admin: "blue", member: "gray", owner: "green",
}},
{ field: "createdAt", label: "Joined", format: "date" },
]}
rows={users ?? []}
isLoading={isLoading}
error={error?.message}
emptyMessage="No users found"
sort={sort}
onSortChange={(col) => setSort((prev) =>
prev.column === col
? { column: col, direction: prev.direction === "asc" ? "desc" : "asc" }
: { column: col, direction: "asc" }
)}
hoverable
striped
/>
);
}

Feature-rich data table with sorting, pagination, search, selection, and row actions.

const columns = [
{ field: "name", label: "Name", sortable: true },
{ field: "email", label: "Email" },
{ field: "amount", label: "Amount", format: "currency", prefix: "$", divisor: 100 },
{ field: "status", label: "Status", format: "badge", badgeColors: {
active: "green", inactive: "gray", suspended: "red",
}},
{ field: "avatar", label: "", format: "avatar", avatarField: "avatarUrl", width: "48px" },
{ field: "verified", label: "Verified", format: "boolean" },
{ field: "progress", label: "Progress", format: "progress" },
{ field: "website", label: "Website", format: "link", linkTextField: "websiteLabel" },
];

Column format options: date, number, currency, badge, boolean, avatar, progress, link, code

<DataTableBase
columns={columns}
rows={pageData}
pagination={{
currentPage: page,
totalPages: Math.ceil(total / pageSize),
pageSize,
totalRows: total,
}}
onPageChange={setPage}
/>
<DataTableBase
columns={columns}
rows={filtered}
searchable
search={query}
onSearchChange={setQuery}
/>
const [selected, setSelected] = useState(new Set());
<DataTableBase
columns={columns}
rows={users}
selectable
selection={selected}
onToggleRow={(id) => {
setSelected((prev) => {
const next = new Set(prev);
next.has(id) ? next.delete(id) : next.add(id);
return next;
});
}}
onToggleAll={(allIds) => {
setSelected((prev) => prev.size === allIds.length ? new Set() : new Set(allIds));
}}
bulkActions={[
{ label: "Delete", icon: "trash", variant: "destructive", onAction: (ids) => deleteMany(ids) },
{ label: "Export", icon: "download", onAction: (ids) => exportCsv(ids) },
]}
/>
<DataTableBase
columns={columns}
rows={users}
rowActions={[
{ label: "Edit", icon: "edit", onAction: (row) => openEdit(row) },
{ label: "Delete", icon: "trash", variant: "destructive", onAction: (row) => deleteUser(row.id) },
]}
/>

DataTableBase has built-in loading, error, and empty states:

<DataTableBase
columns={columns}
rows={data ?? []}
isLoading={isLoading}
error={error?.message}
emptyMessage="No users found"
/>

You can also provide custom content for each state:

<DataTableBase
columns={columns}
rows={data ?? []}
isLoading={isFetching}
loadingContent={<p>Fetching users...</p>}
errorContent={<p>Something went wrong. <button onClick={refetch}>Try again</button></p>}
emptyContent={<p>No users match your filters. <button onClick={clearFilters}>Clear filters</button></p>}
/>

Server-side pagination with filtering and sorting

Section titled “Server-side pagination with filtering and sorting”

The most common real-world pattern: a table that fetches pages from the server with search, filters, and sort state all sent as query parameters.

import {
DataTableBase, FilterBarBase, ColumnBase, RowBase, ButtonBase, SpacerBase,
} from "@lastshotlabs/snapshot/ui";
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
interface User {
id: string;
name: string;
email: string;
role: "admin" | "member";
status: "active" | "inactive";
createdAt: string;
}
function UsersPage() {
const [page, setPage] = useState(1);
const [search, setSearch] = useState("");
const [filters, setFilters] = useState<Record<string, string>>({});
const [sort, setSort] = useState({ column: "name", direction: "asc" as "asc" | "desc" });
const pageSize = 20;
// Build query string from all state
const params = new URLSearchParams({
page: String(page),
pageSize: String(pageSize),
sort: sort.column,
order: sort.direction,
...(search && { search }),
...filters,
});
const url = `/users?${params}`;
const { data, isLoading, error, refetch } = useQuery<{ items: User[]; total: number }>({
queryKey: ["/users", page, pageSize, sort.column, sort.direction, search, filters],
queryFn: () => snap.api.get(url),
});
return (
<ColumnBase gap="md">
<RowBase justify="between" align="center">
<h2>Users</h2>
<ButtonBase label="Refresh" icon="refresh-cw" variant="outline" onClick={() => refetch()} />
</RowBase>
<FilterBarBase
showSearch
searchPlaceholder="Search by name or email..."
filters={[
{ key: "role", label: "Role", options: [
{ label: "Admin", value: "admin" },
{ label: "Member", value: "member" },
]},
{ key: "status", label: "Status", options: [
{ label: "Active", value: "active" },
{ label: "Inactive", value: "inactive" },
]},
]}
onChange={({ search: s, filters: f }) => {
setSearch(s ?? "");
setFilters(f ?? {});
setPage(1); // reset to first page on filter change
}}
/>
<DataTableBase
columns={[
{ field: "name", label: "Name", sortable: true },
{ field: "email", label: "Email", sortable: true },
{ field: "role", label: "Role", format: "badge", badgeColors: { admin: "blue", member: "gray" } },
{ field: "status", label: "Status", format: "badge", badgeColors: { active: "green", inactive: "red" } },
{ field: "createdAt", label: "Joined", format: "date", sortable: true },
]}
rows={data?.items ?? []}
isLoading={isLoading}
error={error?.message}
emptyMessage="No users match your filters"
sort={sort}
onSortChange={(col) => setSort((prev) =>
prev.column === col
? { column: col, direction: prev.direction === "asc" ? "desc" : "asc" }
: { column: col, direction: "asc" }
)}
pagination={{
currentPage: page,
totalPages: Math.ceil((data?.total ?? 0) / pageSize),
pageSize,
totalRows: data?.total ?? 0,
}}
onPageChange={setPage}
hoverable
striped
rowActions={[
{ label: "Edit", icon: "edit", onAction: (row) => openEdit(row as User) },
{ label: "Delete", icon: "trash", variant: "destructive", onAction: (row) => confirmDelete(row as User) },
]}
/>
</ColumnBase>
);
}

This composes FilterBarBase above the table. When filters or search change, the page resets to 1 and the query re-fetches with the new parameters. Sorting and pagination are also server-driven — the server receives the full state and returns the matching page.

Vertical list with icons, badges, and actions.

import { ListBase } from "@lastshotlabs/snapshot/ui";
<ListBase
items={[
{ id: "1", title: "Getting started", description: "Learn the basics", icon: "book" },
{ id: "2", title: "API Reference", description: "Full API docs", icon: "code", badge: "New", badgeColor: "blue" },
{ id: "3", title: "Examples", description: "Real-world examples", icon: "layout", href: "/examples" },
]}
variant="bordered"
divider
/>

Variants: default, bordered, card

Activity feed with grouping and pagination.

import { FeedBase } from "@lastshotlabs/snapshot/ui";
<FeedBase
items={activities.map((a) => ({
key: a.id,
title: a.actor,
description: a.action,
timestamp: a.createdAt,
avatar: a.actorAvatar,
raw: a,
}))}
relativeTime
groupBy="day"
pageSize={20}
infinite
itemActions={[
{ label: "Reply", icon: "reply", onAction: (item) => reply(item) },
]}
/>

Data visualization supporting 10 chart types.

import { ChartBase } from "@lastshotlabs/snapshot/ui";
<ChartBase
chartType="bar"
data={[
{ month: "Jan", revenue: 4200, expenses: 3100 },
{ month: "Feb", revenue: 5300, expenses: 2800 },
{ month: "Mar", revenue: 6100, expenses: 3500 },
]}
xKey="month"
series={[
{ key: "revenue", label: "Revenue", color: "#3b82f6" },
{ key: "expenses", label: "Expenses", color: "#ef4444" },
]}
height={300}
legend
grid
/>

Chart types: line, bar, area, pie, donut, radar, scatter, treemap, funnel, sparkline

Single metric display with trend indicator.

import { StatCardBase } from "@lastshotlabs/snapshot/ui";
<StatCardBase
label="Total Revenue"
value="$48,200"
icon="dollar-sign"
trend={{
direction: "up",
value: "+12.5%",
percentage: 12.5,
sentiment: "positive",
}}
/>

Search and dropdown filters that compose with any data component.

import { FilterBarBase } from "@lastshotlabs/snapshot/ui";
<FilterBarBase
showSearch
searchPlaceholder="Search users..."
filters={[
{ key: "role", label: "Role", options: [
{ label: "Admin", value: "admin" },
{ label: "Member", value: "member" },
]},
{ key: "status", label: "Status", multiple: true, options: [
{ label: "Active", value: "active" },
{ label: "Inactive", value: "inactive" },
]},
]}
onChange={({ search, filters }) => {
setQuery(search);
setFilters(filters);
}}
/>

Formatted data display for single records.

import { DetailCardBase } from "@lastshotlabs/snapshot/ui";
<DetailCardBase
title="User Details"
data={user}
fields={[
{ field: "name", label: "Name", value: user.name },
{ field: "email", label: "Email", value: user.email, format: "email", copyable: true },
{ field: "role", label: "Role", value: user.role, format: "badge" },
{ field: "createdAt", label: "Joined", value: user.createdAt, format: "date" },
{ field: "verified", label: "Verified", value: user.verified, format: "boolean" },
]}
actions={[
{ label: "Edit", icon: "edit", onAction: () => openEdit(user) },
]}
/>

Field formats: text, boolean, date, datetime, number, currency, badge, email, url, link, image, list

ComponentDescription
DataTableBaseFull-featured data table
ListBaseVertical item list
FeedBaseActivity feed with grouping
ChartBase10 chart types
StatCardBaseSingle metric card
FilterBarBaseSearch and filter controls
DetailCardBaseFormatted single-record display
EntityPickerBaseEntity selection dropdown
AlertBaseAlert/notification banners
BadgeBaseStatus badges
AvatarBaseUser avatars
AvatarGroupBaseStacked avatar group
TooltipBaseHover tooltips
ProgressBaseProgress bars
SkeletonBaseLoading placeholders
ScrollAreaBaseScrollable container
SeparatorBaseVisual dividers
EmptyStateBaseEmpty state messages
HighlightedTextBaseText highlighting
NotificationBellBaseNotification indicator
SaveIndicatorBaseSave status indicator
FavoriteButtonBaseFavorite toggle
VoteBaseUpvote/downvote