From c8544bd6b07bad266851e7d3faafdcf5bf1830bb Mon Sep 17 00:00:00 2001
From: John Betancur <1385932+jbetancur@users.noreply.github.com>
Date: Tue, 19 May 2026 21:10:03 -0400
Subject: [PATCH 1/8] feat: add onScroll prop to table API and update changelog
for paginationPosition feature (#1319)
---
apps/docs/.astro/content-assets.mjs | 1 -
apps/docs/.astro/content-modules.mjs | 1 -
apps/docs/.astro/content.d.ts | 199 ------------------
apps/docs/.astro/data-store.json | 1 -
apps/docs/.astro/settings.json | 5 -
apps/docs/.astro/types.d.ts | 2 -
.../src/components/demos/OnScrollDemo.tsx | 67 ++++++
apps/docs/src/pages/docs/api.md | 2 +-
apps/docs/src/pages/docs/changelog.astro | 5 +
apps/docs/src/pages/docs/fixed-header.astro | 30 +++
src/DataTable.css | 2 +-
src/themes/base.ts | 3 +-
src/types.ts | 2 +-
13 files changed, 106 insertions(+), 214 deletions(-)
delete mode 100644 apps/docs/.astro/content-assets.mjs
delete mode 100644 apps/docs/.astro/content-modules.mjs
delete mode 100644 apps/docs/.astro/content.d.ts
delete mode 100644 apps/docs/.astro/data-store.json
delete mode 100644 apps/docs/.astro/settings.json
delete mode 100644 apps/docs/.astro/types.d.ts
create mode 100644 apps/docs/src/components/demos/OnScrollDemo.tsx
diff --git a/apps/docs/.astro/content-assets.mjs b/apps/docs/.astro/content-assets.mjs
deleted file mode 100644
index 2b8b8234..00000000
--- a/apps/docs/.astro/content-assets.mjs
+++ /dev/null
@@ -1 +0,0 @@
-export default new Map();
\ No newline at end of file
diff --git a/apps/docs/.astro/content-modules.mjs b/apps/docs/.astro/content-modules.mjs
deleted file mode 100644
index 2b8b8234..00000000
--- a/apps/docs/.astro/content-modules.mjs
+++ /dev/null
@@ -1 +0,0 @@
-export default new Map();
\ No newline at end of file
diff --git a/apps/docs/.astro/content.d.ts b/apps/docs/.astro/content.d.ts
deleted file mode 100644
index c0082cc8..00000000
--- a/apps/docs/.astro/content.d.ts
+++ /dev/null
@@ -1,199 +0,0 @@
-declare module 'astro:content' {
- export interface RenderResult {
- Content: import('astro/runtime/server/index.js').AstroComponentFactory;
- headings: import('astro').MarkdownHeading[];
- remarkPluginFrontmatter: Record;
- }
- interface Render {
- '.md': Promise;
- }
-
- export interface RenderedContent {
- html: string;
- metadata?: {
- imagePaths: Array;
- [key: string]: unknown;
- };
- }
-}
-
-declare module 'astro:content' {
- type Flatten = T extends { [K: string]: infer U } ? U : never;
-
- export type CollectionKey = keyof AnyEntryMap;
- export type CollectionEntry = Flatten;
-
- export type ContentCollectionKey = keyof ContentEntryMap;
- export type DataCollectionKey = keyof DataEntryMap;
-
- type AllValuesOf = T extends any ? T[keyof T] : never;
- type ValidContentEntrySlug = AllValuesOf<
- ContentEntryMap[C]
- >['slug'];
-
- export type ReferenceDataEntry<
- C extends CollectionKey,
- E extends keyof DataEntryMap[C] = string,
- > = {
- collection: C;
- id: E;
- };
- export type ReferenceContentEntry<
- C extends keyof ContentEntryMap,
- E extends ValidContentEntrySlug | (string & {}) = string,
- > = {
- collection: C;
- slug: E;
- };
- export type ReferenceLiveEntry = {
- collection: C;
- id: string;
- };
-
- /** @deprecated Use `getEntry` instead. */
- export function getEntryBySlug<
- C extends keyof ContentEntryMap,
- E extends ValidContentEntrySlug | (string & {}),
- >(
- collection: C,
- // Note that this has to accept a regular string too, for SSR
- entrySlug: E,
- ): E extends ValidContentEntrySlug
- ? Promise>
- : Promise | undefined>;
-
- /** @deprecated Use `getEntry` instead. */
- export function getDataEntryById(
- collection: C,
- entryId: E,
- ): Promise>;
-
- export function getCollection>(
- collection: C,
- filter?: (entry: CollectionEntry) => entry is E,
- ): Promise;
- export function getCollection(
- collection: C,
- filter?: (entry: CollectionEntry) => unknown,
- ): Promise[]>;
-
- export function getLiveCollection(
- collection: C,
- filter?: LiveLoaderCollectionFilterType,
- ): Promise<
- import('astro').LiveDataCollectionResult, LiveLoaderErrorType>
- >;
-
- export function getEntry<
- C extends keyof ContentEntryMap,
- E extends ValidContentEntrySlug | (string & {}),
- >(
- entry: ReferenceContentEntry,
- ): E extends ValidContentEntrySlug
- ? Promise>
- : Promise | undefined>;
- export function getEntry<
- C extends keyof DataEntryMap,
- E extends keyof DataEntryMap[C] | (string & {}),
- >(
- entry: ReferenceDataEntry,
- ): E extends keyof DataEntryMap[C]
- ? Promise
- : Promise | undefined>;
- export function getEntry<
- C extends keyof ContentEntryMap,
- E extends ValidContentEntrySlug | (string & {}),
- >(
- collection: C,
- slug: E,
- ): E extends ValidContentEntrySlug
- ? Promise>
- : Promise | undefined>;
- export function getEntry<
- C extends keyof DataEntryMap,
- E extends keyof DataEntryMap[C] | (string & {}),
- >(
- collection: C,
- id: E,
- ): E extends keyof DataEntryMap[C]
- ? string extends keyof DataEntryMap[C]
- ? Promise | undefined
- : Promise
- : Promise | undefined>;
- export function getLiveEntry(
- collection: C,
- filter: string | LiveLoaderEntryFilterType,
- ): Promise, LiveLoaderErrorType>>;
-
- /** Resolve an array of entry references from the same collection */
- export function getEntries(
- entries: ReferenceContentEntry>[],
- ): Promise[]>;
- export function getEntries(
- entries: ReferenceDataEntry[],
- ): Promise[]>;
-
- export function render(
- entry: AnyEntryMap[C][string],
- ): Promise;
-
- export function reference(
- collection: C,
- ): import('astro/zod').ZodEffects<
- import('astro/zod').ZodString,
- C extends keyof ContentEntryMap
- ? ReferenceContentEntry>
- : ReferenceDataEntry
- >;
- // Allow generic `string` to avoid excessive type errors in the config
- // if `dev` is not running to update as you edit.
- // Invalid collection names will be caught at build time.
- export function reference(
- collection: C,
- ): import('astro/zod').ZodEffects;
-
- type ReturnTypeOrOriginal = T extends (...args: any[]) => infer R ? R : T;
- type InferEntrySchema = import('astro/zod').infer<
- ReturnTypeOrOriginal['schema']>
- >;
-
- type ContentEntryMap = {
-
- };
-
- type DataEntryMap = {
-
- };
-
- type AnyEntryMap = ContentEntryMap & DataEntryMap;
-
- type ExtractLoaderTypes = T extends import('astro/loaders').LiveLoader<
- infer TData,
- infer TEntryFilter,
- infer TCollectionFilter,
- infer TError
- >
- ? { data: TData; entryFilter: TEntryFilter; collectionFilter: TCollectionFilter; error: TError }
- : { data: never; entryFilter: never; collectionFilter: never; error: never };
- type ExtractDataType = ExtractLoaderTypes['data'];
- type ExtractEntryFilterType = ExtractLoaderTypes['entryFilter'];
- type ExtractCollectionFilterType = ExtractLoaderTypes['collectionFilter'];
- type ExtractErrorType = ExtractLoaderTypes['error'];
-
- type LiveLoaderDataType =
- LiveContentConfig['collections'][C]['schema'] extends undefined
- ? ExtractDataType
- : import('astro/zod').infer<
- Exclude
- >;
- type LiveLoaderEntryFilterType =
- ExtractEntryFilterType;
- type LiveLoaderCollectionFilterType =
- ExtractCollectionFilterType;
- type LiveLoaderErrorType = ExtractErrorType<
- LiveContentConfig['collections'][C]['loader']
- >;
-
- export type ContentConfig = typeof import("../src/content.config.mjs");
- export type LiveContentConfig = never;
-}
diff --git a/apps/docs/.astro/data-store.json b/apps/docs/.astro/data-store.json
deleted file mode 100644
index 0caf6209..00000000
--- a/apps/docs/.astro/data-store.json
+++ /dev/null
@@ -1 +0,0 @@
-[["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.18.1","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"static\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":4321,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true,\"allowedDomains\":[],\"actionBodySizeLimit\":1048576},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"headingIdCompat\":false,\"preserveScriptOrder\":false,\"liveContentCollections\":false,\"csp\":false,\"staticImportMetaEnv\":false,\"chromeDevtoolsWorkspace\":false,\"failOnPrerenderConflict\":false,\"svgo\":false},\"legacy\":{\"collections\":false}}"]
\ No newline at end of file
diff --git a/apps/docs/.astro/settings.json b/apps/docs/.astro/settings.json
deleted file mode 100644
index 89086c3d..00000000
--- a/apps/docs/.astro/settings.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "_variables": {
- "lastUpdateCheck": 1778034170906
- }
-}
\ No newline at end of file
diff --git a/apps/docs/.astro/types.d.ts b/apps/docs/.astro/types.d.ts
deleted file mode 100644
index 03d7cc43..00000000
--- a/apps/docs/.astro/types.d.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-///
-///
\ No newline at end of file
diff --git a/apps/docs/src/components/demos/OnScrollDemo.tsx b/apps/docs/src/components/demos/OnScrollDemo.tsx
new file mode 100644
index 00000000..56c3b182
--- /dev/null
+++ b/apps/docs/src/components/demos/OnScrollDemo.tsx
@@ -0,0 +1,67 @@
+import { useState } from 'react';
+import DataTable from '../ThemedDataTable';
+import { type TableColumn } from 'react-data-table-component';
+
+interface Employee {
+ id: number;
+ name: string;
+ department: string;
+ salary: number;
+ status: string;
+}
+
+const data: Employee[] = Array.from({ length: 40 }, (_, i) => ({
+ id: i + 1,
+ name:
+ [
+ 'Aria Chen',
+ 'Marcus Webb',
+ 'Priya Kapoor',
+ 'Jordan Ellis',
+ 'Sam Rivera',
+ 'Taylor Brooks',
+ 'Casey Morgan',
+ 'Alex Kim',
+ 'Morgan Lee',
+ 'Drew Park',
+ ][i % 10] + (i >= 10 ? ` ${Math.floor(i / 10) + 1}` : ''),
+ department: ['Engineering', 'Product', 'Design', 'Analytics', 'Sales', 'HR'][i % 6],
+ salary: 80000 + ((i * 1237) % 70000),
+ status: ['Active', 'Remote', 'On Leave', 'Contractor'][i % 4],
+}));
+
+const columns: TableColumn[] = [
+ { name: '#', selector: r => r.id, width: '60px' },
+ { name: 'Name', selector: r => r.name, sortable: true },
+ { name: 'Department', selector: r => r.department, sortable: true },
+ {
+ name: 'Salary',
+ selector: r => r.salary,
+ format: r => `$${r.salary.toLocaleString()}`,
+ right: true,
+ sortable: true,
+ },
+ { name: 'Status', selector: r => r.status },
+];
+
+export default function OnScrollDemo() {
+ const [scrollTop, setScrollTop] = useState(0);
+
+ return (
+
+
+ scrollTop: {scrollTop}px
+
+
+ setScrollTop(Math.round((e.target as HTMLDivElement).scrollTop))}
+ />
+
+ );
+}
diff --git a/apps/docs/src/pages/docs/api.md b/apps/docs/src/pages/docs/api.md
index 798f5028..577d54ba 100644
--- a/apps/docs/src/pages/docs/api.md
+++ b/apps/docs/src/pages/docs/api.md
@@ -47,6 +47,7 @@ Complete reference for every prop, type, and export in `react-data-table-compone
| `responsive` | `boolean` | `true` | Wrap the table in a horizontally scrollable container. |
| `fixedHeader` | `boolean` | `false` | Stick the column header at the top when scrolling. |
| `fixedHeaderScrollHeight` | `string` | `"100vh"` | Max height of the scrollable body when `fixedHeader` is on. |
+| `onScroll` | `(event) => void` | - | Called when the user scrolls the table body. Works with both `fixedHeader` enabled and disabled. |
| `direction` | `Direction` | `"ltr"` | Text direction (`ltr`, `rtl`, `auto`). |
| `className` | `string` | - | Extra CSS class on the root element. |
| `style` | `CSSProperties` | - | Inline styles on the root element. |
@@ -150,7 +151,6 @@ Column-level footers live on each [`TableColumn`](#tablecolumnt) as the `foot
| `onRowMiddleClicked` | `(row, event) => void` | Called when a row is middle-clicked (scroll-click). Use with `onRowClicked` to implement open-in-new-tab behaviour. |
| `onRowMouseEnter` | `(row, event) => void` | Called when the pointer enters a row. |
| `onRowMouseLeave` | `(row, event) => void` | Called when the pointer leaves a row. |
-| `onScroll` | `(event) => void` | Called when the user scrolls the table body. Works with both `fixedHeader` enabled and disabled. |
### Column features
diff --git a/apps/docs/src/pages/docs/changelog.astro b/apps/docs/src/pages/docs/changelog.astro
index a7dd5109..f3746e87 100644
--- a/apps/docs/src/pages/docs/changelog.astro
+++ b/apps/docs/src/pages/docs/changelog.astro
@@ -15,6 +15,11 @@ import CodeBlock from '../../components/CodeBlock.astro';
New features
+
+ paginationPosition — controls where the pagination bar renders relative to the
+ table. Accepts 'bottom' (default), 'top', or 'both'.
+ → Pagination docs
+
paginationPage — controlled active-page prop. Set it to navigate the table
programmatically (e.g. reset to page 1 after a filter change). Use together with
diff --git a/apps/docs/src/pages/docs/fixed-header.astro b/apps/docs/src/pages/docs/fixed-header.astro
index 8df199aa..7ef652a2 100644
--- a/apps/docs/src/pages/docs/fixed-header.astro
+++ b/apps/docs/src/pages/docs/fixed-header.astro
@@ -3,6 +3,7 @@ import DocsLayout from '../../layouts/DocsLayout.astro';
import Demo from '../../components/Demo.astro';
import CodeBlock from '../../components/CodeBlock.astro';
import FixedHeaderDemo from '../../components/demos/FixedHeaderDemo.tsx';
+import OnScrollDemo from '../../components/demos/OnScrollDemo.tsx';
---
@@ -82,6 +83,29 @@ const columns: TableColumn[] = [
selectableRows
/>`} />
+
Tracking scroll position with onScroll
+
+ The onScroll prop fires on every scroll event inside the table body. It receives a
+ standard React.UIEvent<HTMLDivElement> so you can read scrollTop,
+ scrollLeft, or any other scroll geometry from event.target.
+
+
+
+ {lastSort ?? 'Sort by any column, then reset'}
+
+
+ setLastSort(`sorted by "${col.id}" ${dir}`)}
+ highlightOnHover
+ />
+
+ );
+}
diff --git a/apps/docs/src/pages/docs/api.md b/apps/docs/src/pages/docs/api.md
index 577d54ba..b1c518eb 100644
--- a/apps/docs/src/pages/docs/api.md
+++ b/apps/docs/src/pages/docs/api.md
@@ -78,6 +78,7 @@ Complete reference for every prop, type, and export in `react-data-table-compone
| `sortFunction` | `SortFunction \| null` | - | Global custom sort function applied to all sortable columns. |
| `sortIcon` | `ReactNode` | built-in chevron | Custom sort direction indicator. |
| `onSort` | `(column, direction, sortedRows) => void` | - | Called whenever the sort column or direction changes. |
+| `ref.clearSort()` | `DataTableHandle` | - | Imperatively reset sort to the default state. See [DataTableHandle](#datatablehandle-ref). |
### Pagination
@@ -125,6 +126,7 @@ Column-level footers live on each [`TableColumn`](#tablecolumnt) as the `foot
| `selectableRowsComponentProps` | `object` | - | Extra props forwarded to the custom checkbox component. |
| `onSelectedRowsChange` | `(state) => void` | - | Called whenever selection changes. Receives `{ allSelected, selectedCount, selectedRows }`. |
| `clearSelectedRows` | `boolean` | - | **Deprecated.** Toggle to clear selection. Use `ref.current.clearSelectedRows()` instead. |
+| `ref.clearSelectedRows()` | `DataTableHandle` | - | Imperatively deselect all selected rows. See [DataTableHandle](#datatablehandle-ref). |
### Expandable rows
@@ -312,9 +314,8 @@ function App() {
return (
<>
-
+
+
>
);
@@ -324,6 +325,7 @@ function App() {
| Method | Description |
|---|---|
| `clearSelectedRows()` | Deselect all currently selected rows. |
+| `clearSort()` | Reset sort to the default (`defaultSortFieldId` / `defaultSortAsc`), or unsorted if no defaults are set. |
## TableStyles
diff --git a/apps/docs/src/pages/docs/changelog.astro b/apps/docs/src/pages/docs/changelog.astro
index f3746e87..33f76b94 100644
--- a/apps/docs/src/pages/docs/changelog.astro
+++ b/apps/docs/src/pages/docs/changelog.astro
@@ -39,6 +39,22 @@ import CodeBlock from '../../components/CodeBlock.astro';
and "Last Page" are now configurable via paginationComponentOptions, enabling
proper i18n for screen readers.
+
+ ref.clearSort() — new DataTableHandle method to programmatically
+ reset sort back to its default state (defaultSortFieldId / defaultSortAsc),
+ or unsorted if no defaults are set.
+ → Sorting docs
+
+
+ Sortable column indicator — sortable columns now show a faint sort icon at
+ reduced opacity so users can discover which columns are sortable before clicking. The inactive
+ opacity is themable via the --rdt-sort-icon-inactive-opacity CSS custom property
+ (default 0.3).
+
+
+ onScroll — new prop that fires whenever the table's scroll wrapper scrolls.
+ Receives the native React.UIEvent<HTMLDivElement>.
+
Bug fixes
diff --git a/apps/docs/src/pages/docs/sorting.astro b/apps/docs/src/pages/docs/sorting.astro
index 122a33cd..808cda5c 100644
--- a/apps/docs/src/pages/docs/sorting.astro
+++ b/apps/docs/src/pages/docs/sorting.astro
@@ -6,6 +6,7 @@ import SortingDemo from '../../components/demos/SortingDemo.tsx';
import PrioritySortDemo from '../../components/demos/PrioritySortDemo.tsx';
import ServerSideSortingDemo from '../../components/demos/ServerSideSortingDemo.tsx';
import ServerSideSortPaginationDemo from '../../components/demos/ServerSideSortPaginationDemo.tsx';
+import SortResetDemo from '../../components/demos/SortResetDemo.tsx';
---
@@ -323,12 +324,46 @@ export default function App() {
+
Resetting sort programmatically
+
+ Attach a ref to DataTable and call ref.current.clearSort() to reset
+ the sort back to its default state (defaultSortFieldId / defaultSortAsc,
+ or unsorted if no defaults are set). Useful when the user switches a view mode that makes the
+ current sort invalid.
+
+ A data table is fundamentally a desktop pattern — comparing values across rows requires seeing
+ multiple columns at once. On small screens the table stays a table, but several defaults are
+ tuned to make that experience as comfortable as possible.
+
+ The table wraps in a horizontally-scrollable container with momentum scrolling and
+ scroll-chaining prevention. On touch devices users can swipe left/right to reveal hidden columns
+ without disrupting the page's vertical scroll.
+
+
+
Hiding columns on small screens
+
+
+ Use the hide property on a column to remove it below a breakpoint.
+ The column header and all its cells disappear — the remaining columns fill the available width.
+
+ You can also pass a numeric pixel value to hide at a custom breakpoint:
+
+
+ r.notes, hide: 480 }`} />
+
+
Pagination on small screens
+
+
+ Below 600px the rows-per-page label and the current-range text (1–5 of 30) are hidden
+ automatically, giving the pagination controls more breathing room. The rows-per-page select and
+ nav buttons remain.
+
+
+
Touch targets
+
+
+ Row height enforces a minimum of 48px on small screens so tap targets meet accessibility
+ guidelines. Pagination buttons expand to 44×44px. Cell padding tightens slightly
+ (--rdt-cell-padding-x-mobile, default 12px) to reclaim horizontal space.
+
+
+
CSS custom properties
+
+
You can override the mobile padding via CSS variables on the table container:
+
+
+
+
Recommendations for app developers
+
+
+ For apps where the primary use case on mobile is looking up a single record rather than
+ comparing rows, consider rendering a completely different component below your breakpoint —
+ a simple list or card feed — and only mounting DataTable on wider viewports:
+