Skip to content

Commit c65efc4

Browse files
authored
add mobile layout support and demo for responsive data table (jbetancur#1321)
1 parent 7a73cc7 commit c65efc4

4 files changed

Lines changed: 279 additions & 0 deletions

File tree

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import React, { useState } from 'react';
2+
import DataTable from '../ThemedDataTable';
3+
import { type TableColumn } from 'react-data-table-component';
4+
5+
interface Employee {
6+
id: number;
7+
name: string;
8+
department: string;
9+
salary: number;
10+
status: 'Active' | 'Remote' | 'Contractor' | 'On Leave';
11+
}
12+
13+
const data: Employee[] = Array.from({ length: 20 }, (_, i) => ({
14+
id: i + 1,
15+
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}` : ''),
16+
department: ['Engineering', 'Product', 'Design', 'Analytics', 'Sales', 'HR'][i % 6],
17+
salary: 80000 + i * 4200,
18+
status: (['Active', 'Remote', 'Contractor', 'On Leave'] as const)[i % 4],
19+
}));
20+
21+
const statusColors: Record<Employee['status'], string> = {
22+
Active: 'bg-green-100 text-green-700',
23+
Remote: 'bg-blue-100 text-blue-700',
24+
Contractor: 'bg-yellow-100 text-yellow-700',
25+
'On Leave': 'bg-gray-100 text-gray-600',
26+
};
27+
28+
export default function MobileDemo() {
29+
const [width, setWidth] = useState<'375' | '430' | '100%'>('375');
30+
const [variant, setVariant] = useState<'basic' | 'selection' | 'pagination'>('basic');
31+
32+
const isNarrow = width === '375' || width === '430';
33+
34+
const baseColumns: TableColumn<Employee>[] = [
35+
{
36+
name: 'Name',
37+
selector: r => r.name,
38+
sortable: true,
39+
grow: 1.5,
40+
},
41+
{
42+
name: 'Dept',
43+
selector: r => r.department,
44+
sortable: true,
45+
omit: isNarrow,
46+
},
47+
{
48+
name: 'Salary',
49+
selector: r => r.salary,
50+
sortable: true,
51+
right: true,
52+
format: r => `$${r.salary.toLocaleString()}`,
53+
omit: isNarrow,
54+
},
55+
{
56+
name: 'Status',
57+
selector: r => r.status,
58+
center: true,
59+
cell: r => (
60+
<span className={`px-1.5 py-0.5 rounded-full text-xs font-medium ${statusColors[r.status]}`}>
61+
{r.status}
62+
</span>
63+
),
64+
},
65+
];
66+
67+
const btnBase = 'px-2.5 py-1 rounded-md text-xs font-medium border transition-colors cursor-pointer';
68+
const btnOn = 'bg-brand-600 text-white border-brand-600';
69+
const btnOff = 'bg-white text-gray-600 border-gray-200 hover:border-gray-300';
70+
71+
return (
72+
<div className="space-y-4">
73+
<div className="flex flex-wrap gap-3 items-center">
74+
<div className="flex items-center gap-1.5">
75+
<span className="text-xs text-gray-500">Viewport:</span>
76+
{(['375', '430', '100%'] as const).map(w => (
77+
<button key={w} className={`${btnBase} ${width === w ? btnOn : btnOff}`} onClick={() => setWidth(w)}>
78+
{w === '100%' ? 'Full' : `${w}px`}
79+
</button>
80+
))}
81+
</div>
82+
<div className="flex items-center gap-1.5">
83+
<span className="text-xs text-gray-500">Mode:</span>
84+
{(['basic', 'selection', 'pagination'] as const).map(v => (
85+
<button key={v} className={`${btnBase} ${variant === v ? btnOn : btnOff}`} onClick={() => setVariant(v)}>
86+
{v}
87+
</button>
88+
))}
89+
</div>
90+
</div>
91+
92+
<div
93+
className="border border-dashed border-gray-300 rounded-lg overflow-hidden bg-white mx-auto transition-all"
94+
style={{ width: width === '100%' ? '100%' : `${width}px`, maxWidth: '100%' }}
95+
>
96+
{width !== '100%' && (
97+
<div className="bg-gray-100 px-3 py-1.5 flex items-center justify-between border-b border-gray-200">
98+
<span className="text-xs text-gray-400 font-mono">{width}px viewport</span>
99+
<div className="flex gap-1">
100+
<div className="w-2 h-2 rounded-full bg-red-400" />
101+
<div className="w-2 h-2 rounded-full bg-yellow-400" />
102+
<div className="w-2 h-2 rounded-full bg-green-400" />
103+
</div>
104+
</div>
105+
)}
106+
<DataTable
107+
columns={baseColumns}
108+
data={data}
109+
title={variant === 'basic' ? 'Team' : undefined}
110+
selectableRows={variant === 'selection'}
111+
pagination={variant === 'pagination'}
112+
paginationPerPage={5}
113+
paginationRowsPerPageOptions={[5, 10]}
114+
highlightOnHover
115+
striped
116+
dense={width === '375'}
117+
/>
118+
</div>
119+
120+
{isNarrow && (
121+
<p className="text-xs text-gray-400 text-center">
122+
Department and Salary are hidden at this width — use <code className="bg-gray-100 px-1 rounded">omit</code> or <code className="bg-gray-100 px-1 rounded">hide: &quot;sm&quot;</code> to drop columns on small screens.
123+
</p>
124+
)}
125+
</div>
126+
);
127+
}

apps/docs/src/layouts/DocsLayout.astro

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const nav = [
6060
{ label: 'Animations', href: '/docs/animations' },
6161
{ label: 'Accessibility', href: '/docs/accessibility' },
6262
{ label: 'RTL Support', href: '/docs/rtl' },
63+
{ label: 'Mobile', href: '/docs/mobile' },
6364
],
6465
},
6566
{
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
---
2+
import DocsLayout from '../../layouts/DocsLayout.astro';
3+
import Demo from '../../components/Demo.astro';
4+
import CodeBlock from '../../components/CodeBlock.astro';
5+
import MobileDemo from '../../components/demos/MobileDemo.tsx';
6+
---
7+
8+
<DocsLayout title="Mobile | react-data-table-component">
9+
<h1>Mobile</h1>
10+
11+
<p>
12+
A data table is fundamentally a desktop pattern — comparing values across rows requires seeing
13+
multiple columns at once. On small screens the table stays a table, but several defaults are
14+
tuned to make that experience as comfortable as possible.
15+
</p>
16+
17+
<Demo
18+
title="Mobile layout preview"
19+
description="Toggle between 375px, 430px, and full width to see how the table adapts. Switch modes to try selection and pagination at each size."
20+
code={`import DataTable, { type TableColumn } from 'react-data-table-component';
21+
22+
const columns: TableColumn<Employee>[] = [
23+
{ name: 'Name', selector: r => r.name, sortable: true },
24+
{ name: 'Department', selector: r => r.department, hide: 'sm' },
25+
{ name: 'Salary', selector: r => r.salary, right: true, hide: 'sm',
26+
format: r => \`$\${r.salary.toLocaleString()}\` },
27+
{ name: 'Status', selector: r => r.status, center: true,
28+
cell: r => <StatusBadge status={r.status} /> },
29+
];
30+
31+
<DataTable columns={columns} data={data} striped highlightOnHover />`}
32+
>
33+
<MobileDemo client:only="react" />
34+
</Demo>
35+
36+
<h2>How it works</h2>
37+
38+
<p>
39+
The table wraps in a horizontally-scrollable container with momentum scrolling and
40+
scroll-chaining prevention. On touch devices users can swipe left/right to reveal hidden columns
41+
without disrupting the page's vertical scroll.
42+
</p>
43+
44+
<h2>Hiding columns on small screens</h2>
45+
46+
<p>
47+
Use the <code>hide</code> property on a column to remove it below a breakpoint.
48+
The column header and all its cells disappear — the remaining columns fill the available width.
49+
</p>
50+
51+
<CodeBlock code={`const columns: TableColumn<Row>[] = [
52+
{ name: 'Name', selector: r => r.name }, // always visible
53+
{ name: 'Email', selector: r => r.email, hide: 'sm' }, // hidden below 600px
54+
{ name: 'Salary', selector: r => r.salary, hide: 'md' }, // hidden below 960px
55+
{ name: 'Joined', selector: r => r.joined, hide: 'lg' }, // hidden below 1280px
56+
];`} />
57+
58+
<p>
59+
You can also pass a numeric pixel value to hide at a custom breakpoint:
60+
</p>
61+
62+
<CodeBlock code={`{ name: 'Notes', selector: r => r.notes, hide: 480 }`} />
63+
64+
<h2>Pagination on small screens</h2>
65+
66+
<p>
67+
Below 600px the rows-per-page label and the current-range text (<em>1–5 of 30</em>) are hidden
68+
automatically, giving the pagination controls more breathing room. The rows-per-page select and
69+
nav buttons remain.
70+
</p>
71+
72+
<h2>Touch targets</h2>
73+
74+
<p>
75+
Row height enforces a minimum of 48px on small screens so tap targets meet accessibility
76+
guidelines. Pagination buttons expand to 44×44px. Cell padding tightens slightly
77+
(<code>--rdt-cell-padding-x-mobile</code>, default 12px) to reclaim horizontal space.
78+
</p>
79+
80+
<h2>CSS custom properties</h2>
81+
82+
<p>You can override the mobile padding via CSS variables on the table container:</p>
83+
84+
<CodeBlock lang="css" code={`:root {
85+
--rdt-cell-padding-x-mobile: 8px; /* tighter cells on very small screens */
86+
}`} />
87+
88+
<h2>Recommendations for app developers</h2>
89+
90+
<p>
91+
For apps where the primary use case on mobile is looking up a single record rather than
92+
comparing rows, consider rendering a completely different component below your breakpoint —
93+
a simple list or card feed — and only mounting <code>DataTable</code> on wider viewports:
94+
</p>
95+
96+
<CodeBlock code={`import { useWindowSize } from 'react-data-table-component';
97+
98+
function MyPage() {
99+
const { width } = useWindowSize();
100+
101+
if (width !== undefined && width < 600) {
102+
return <MobileList data={data} />;
103+
}
104+
105+
return <DataTable columns={columns} data={data} />;
106+
}`} />
107+
</DocsLayout>

src/DataTable.css

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
min-height: 0;
4141
overscroll-behavior-x: contain;
4242
touch-action: pan-x pan-y;
43+
-webkit-overflow-scrolling: touch;
4344
}
4445

4546
.rdt_responsiveWrapperFixed {
@@ -398,6 +399,8 @@
398399
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.14);
399400
padding: 10px;
400401
min-width: 260px;
402+
max-width: calc(100vw - 24px);
403+
box-sizing: border-box;
401404
}
402405

403406
/* A single condition row: [select] [input] ([and] [input]) [remove] */
@@ -975,6 +978,19 @@
975978
min-height: 56px;
976979
padding-left: 16px;
977980
padding-right: 8px;
981+
gap: 4px;
982+
}
983+
984+
@media (max-width: 599px) {
985+
.rdt_header {
986+
font-size: 18px;
987+
padding-left: 12px;
988+
padding-right: 6px;
989+
}
990+
991+
.rdt_headerActions {
992+
flex: 0 0 auto;
993+
}
978994
}
979995

980996
.rdt_headerTitle {
@@ -1193,6 +1209,17 @@
11931209
width: 100%;
11941210
justify-content: space-around;
11951211
}
1212+
1213+
.rdt_paginationRange,
1214+
.rdt_paginationRowLabel {
1215+
display: none;
1216+
}
1217+
1218+
.rdt_pagination {
1219+
justify-content: center;
1220+
padding-left: 4px;
1221+
padding-right: 4px;
1222+
}
11961223
}
11971224

11981225
.rdt_paginationSpan {
@@ -1277,6 +1304,23 @@
12771304
background-color: var(--rdt-color-bg, #fff);
12781305
}
12791306

1307+
/* ─── Mobile touch targets ───────────────────────────────────────────────────── */
1308+
@media (max-width: 599px) {
1309+
.rdt_row {
1310+
min-height: max(var(--rdt-row-height, 48px), 48px);
1311+
}
1312+
1313+
.rdt_cellBase {
1314+
padding-left: var(--rdt-cell-padding-x-mobile, 12px);
1315+
padding-right: var(--rdt-cell-padding-x-mobile, 12px);
1316+
}
1317+
1318+
.rdt_paginationButton {
1319+
height: 44px;
1320+
width: 44px;
1321+
}
1322+
}
1323+
12801324
/* ─── Responsive column hiding ───────────────────────────────────────────────── */
12811325
@media (max-width: 599px) {
12821326
.rdt_hideOnSm {

0 commit comments

Comments
 (0)